From a7752c1a6cb23999b3b0b2fadde10bd4b6edfaab Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 27 Apr 2020 16:02:19 +0200 Subject: [PATCH 0001/1681] bumped version to 0.8.1 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 38dd8e61a..7f52d887a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.8.0' +version = '0.8.1' # The full version, including alpha/beta/rc tags. -release = '0.8.0' +release = '0.8.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index 4b41f137f..b5c61ee08 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 8, 0) +__version_info__ = (0, 8, 1) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 6a82d8ace7dfbcfe4d51b1d5826bf9fd92d2949e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 27 Apr 2020 18:05:39 +0200 Subject: [PATCH 0002/1681] make sure that the tests are actually executed in tox with Python 2.7 --- setup.py | 3 +++ src/_igraph/attributes.c | 7 +++++-- tests/test_attributes.py | 4 +++- tests/test_foreign.py | 2 +- tests/utils.py | 4 ++-- tox.ini | 3 +++ 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 29482e21d..98465b559 100644 --- a/setup.py +++ b/setup.py @@ -836,6 +836,9 @@ def use_educated_guess(self): }, ) +if sys.version_info <= (2, 7): + options["requires"] = options.pop("install_requires") + if sys.version_info > (3, 0): options["use_2to3"] = True diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index fd9431834..db746334c 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -87,8 +87,11 @@ int igraphmodule_i_attribute_struct_index_vertex_names( PyErr_Format( PyExc_RuntimeError, "error while indexing vertex names; did you accidentally try to " - "use a non-hashable object as a vertex name earlier? " - "Check the name of vertex %R (%R)", value, key + "use a non-hashable object as a vertex name earlier?" +#ifdef IGRAPH_PYTHON3 + /* %R is not supported in Python 2.x */ + " Check the name of vertex %R (%R)", value, key +#endif ); } diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 45984a737..4d81ee18f 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -1,4 +1,5 @@ # vim:ts=4 sw=4 sts=4: +import sys import unittest from igraph import * @@ -102,7 +103,8 @@ def testUnhashableVertexNames(self): # Check the exception self.assertTrue(isinstance(err, RuntimeError)) - self.assertTrue(repr(value) in str(err)) + if sys.version_info >= (3, 4): + self.assertTrue(repr(value) in str(err)) def testVertexNameIndexingBug196(self): g = Graph() diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 5c4ff1d30..ff30ec2af 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -226,7 +226,7 @@ def testPickle(self): pickle = bytes(pickle) else: pickle = "".join(map(chr, pickle)) - with temporary_file(pickle, "wb") as tmpfname: + with temporary_file(pickle, "wb", binary=True) as tmpfname: g = Graph.Read_Pickle(pickle) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and diff --git a/tests/utils.py b/tests/utils.py index c4fab472f..82b951a54 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -47,7 +47,7 @@ def skipIf(condition, reason): @contextmanager -def temporary_file(content=None, mode=None): +def temporary_file(content=None, mode=None, binary=False): tmpf, tmpfname = tempfile.mkstemp() os.close(tmpf) @@ -59,7 +59,7 @@ def temporary_file(content=None, mode=None): tmpf = open(tmpfname, mode) if content is not None: - if hasattr(content, "encode"): + if hasattr(content, "encode") and not binary: tmpf.write(dedent(content).encode("utf8")) else: tmpf.write(content) diff --git a/tox.ini b/tox.ini index 37d5c3e86..d2c11f3e3 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,9 @@ deps = setenv = TESTING_IN_TOX=1 +[testenv:py27] +commands = python -m unittest discover + [flake8] max-line-length = 80 select = C,E,F,W,B,B950 From c796a105d2fbeb331c46e8cc2390e04716416fb5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Apr 2020 11:38:44 +0200 Subject: [PATCH 0003/1681] update embedded igraph to dev version to fix compilation problems on MSVC --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 48b1c706b..578579185 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 48b1c706b4364c40678a3e01fde1766380dcfabd +Subproject commit 5785791853de2f9653161e5f5f035256f76dddb0 From 4b2bfbe8459c976c73e722aeb7ec78f52b1e12d7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Apr 2020 13:10:19 +0200 Subject: [PATCH 0004/1681] try again with further tweaks --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 578579185..0afacadd8 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 5785791853de2f9653161e5f5f035256f76dddb0 +Subproject commit 0afacadd854aefc3db9758a7a46863b4d851aacd From dee73ba0a5141047453718e4c9b1e75f08a2244f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Apr 2020 14:34:34 +0200 Subject: [PATCH 0005/1681] bumped version to 0.8.2 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7f52d887a..5b83e8332 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.8.1' +version = '0.8.2' # The full version, including alpha/beta/rc tags. -release = '0.8.1' +release = '0.8.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index b5c61ee08..b523d76db 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 8, 1) +__version_info__ = (0, 8, 2) __version__ = ".".join("{0}".format(x) for x in __version_info__) From ad6e83f972a36ec82536c0b6db29b6913452429a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 30 Apr 2020 22:32:14 +0200 Subject: [PATCH 0006/1681] don't leak full absolute paths in warning messages, refs #293 --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 98465b559..58d3df338 100644 --- a/setup.py +++ b/setup.py @@ -263,6 +263,8 @@ def compile_in(self, build_folder, source_folder=None): source_folder = os.path.abspath(source_folder) build_folder = os.path.abspath(build_folder) + + build_to_source_folder = os.path.relpath(source_folder, build_folder) cwd = os.getcwd() try: @@ -297,7 +299,7 @@ def compile_in(self, build_folder, source_folder=None): configure_args.extend(os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ")) retcode = subprocess.call( "sh {0} {1}".format( - quote_path_for_shell(os.path.join(source_folder, "configure")), + quote_path_for_shell(os.path.join(build_to_source_folder, "configure")), " ".join(configure_args) ), env=self.enhanced_env(CFLAGS="-fPIC", CXXFLAGS="-fPIC"), From b6b8d6d6d2f707055f8f8f37a159ec6a8b8376d6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 7 May 2020 21:35:42 +0200 Subject: [PATCH 0007/1681] fix typo in installation instructions, closes #26 --- doc/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 97c002cfb..45c26fbd9 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -64,7 +64,7 @@ Users of the `Anaconda Python distribution `_ may opt to install |igraph|'s Python interface using conda. That can be achieved by running the following command: - $ conda install -c conda-forge python-graph + $ conda install -c conda-forge python-igraph To test the installed package, launch Python and run the following: From b94ff802ece20f00107c4d17dcb0d72578e2b349 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 May 2020 12:47:15 +0200 Subject: [PATCH 0008/1681] trying to install zstd in Appveyor to support installing .tar.zst packages --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 65deb3354..a602baa12 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,6 +25,7 @@ platform: install: - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors" - bash -lc "pacman --noconfirm -Sy" + - bash -lc "pacman --noconfirm -S zstd" - bash -lc "pacman --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - pip install cibuildwheel==1.1.0 From 9a826d6fc463ee7c74332dba510826431ae6ea4b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Jun 2020 12:24:57 +0200 Subject: [PATCH 0009/1681] fix an issue with 'detected' being referenced befure it is defined; closes #299 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 58d3df338..0515be342 100644 --- a/setup.py +++ b/setup.py @@ -487,6 +487,7 @@ def run(self): # Check whether the user asked us to discover a pre-built igraph # with pkg-config + detected = False if buildcfg.use_pkgconfig: detected = buildcfg.detect_from_pkgconfig() if not detected: From 5f71ad9716dda3470e6667b559ca0495de64e700 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jun 2020 13:10:09 +0200 Subject: [PATCH 0010/1681] added supported Python versions to setup.py --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 0515be342..24806c863 100644 --- a/setup.py +++ b/setup.py @@ -825,6 +825,11 @@ def use_educated_guess(self): "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: Python", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Mathematics", From 0bf67030de0de11bcc4ba44807847fc631588fd4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jun 2020 13:14:11 +0200 Subject: [PATCH 0011/1681] clarified supported Python versions, closes #300 --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40c263a5e..05e01b552 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![](https://travis-ci.org/igraph/python-igraph.svg?branch=master)](https://travis-ci.org/igraph/python-igraph) +[![PyPI pyversions](https://img.shields.io/badge/python-2.7%20%7C%203.5%20%7C%203.6%20%7C%203.7%20%7C%203.8-blue)](https://pypi.python.org/pypi/python-igraph) +[![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) Python interface for the igraph library --------------------------------------- @@ -149,7 +151,23 @@ faster than the first one as the C core does not need to be recompiled. ## Notes -### Pypy +### Supported Python versions + +We aim to keep up with the development cycle of Python and support all official +Python versions that have not reached their end of life yet. Currently this +means that we support Python 3.5 to 3.8, inclusive. Please refer to [this +page](https://devguide.python.org/#branchstatus) for the status of Python +branches and let us know if you encounter problems with `python-igraph` on any +of the non-EOL Python versions. + +Continuous integration tests are regularly executed on all non-EOL Python +branches. + +As for Python 2.x, the latest branch of `python-igraph` that supports Python 2 +is the 0.8.x series. Python 2 support will be dropped with the release of +`python-igraph` 0.9. + +### PyPy This version of python-igraph is compatible with [PyPy](http://pypy.org/) and is regularly tested on [PyPy](http://pypy.org/) with ``tox``. However, the From 6318ee311cb230797e96419b0c96a3aa587e7ddb Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Jun 2020 13:15:56 +0200 Subject: [PATCH 0012/1681] updated Travis badge to match the style of the other badges --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05e01b552..f8b13693b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![](https://travis-ci.org/igraph/python-igraph.svg?branch=master)](https://travis-ci.org/igraph/python-igraph) +[![Travis CI](https://img.shields.io/travis/igraph/python-igraph)](https://travis-ci.org/igraph/python-igraph) [![PyPI pyversions](https://img.shields.io/badge/python-2.7%20%7C%203.5%20%7C%203.6%20%7C%203.7%20%7C%203.8-blue)](https://pypi.python.org/pypi/python-igraph) [![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) From 6bbae1fbee2eb240215e57d9e2ae7964574a0360 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 11 Jun 2020 14:09:38 +0200 Subject: [PATCH 0013/1681] updated vendored igraph to incorporate some recent bugfixes --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 0afacadd8..20ad88e75 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 0afacadd854aefc3db9758a7a46863b4d851aacd +Subproject commit 20ad88e759205e73d79f612663153ba97c56a866 From 8ff48a813b86f684b2eb1ca56552d643d6df4417 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Sat, 27 Jun 2020 14:44:06 +0200 Subject: [PATCH 0014/1681] Clarified and corrected negative number of iterations in Leiden algorithm (#304) --- src/_igraph/graphobject.c | 10 +++++----- src/igraph/__init__.py | 19 +++++++++++-------- vendor/source/igraph | 2 +- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index f341714b9..dac3b4740 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -11753,7 +11753,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, static char *kwlist[] = {"edge_weights", "node_weights", "resolution_parameter", "normalize_resolution", "beta", "initial_membership", "n_iterations", NULL}; - + PyObject *edge_weights_o = Py_None; PyObject *node_weights_o = Py_None; PyObject *initial_membership_o = Py_None; @@ -11823,12 +11823,12 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, } } } - resolution_parameter /= igraph_vector_sum(node_weights); + resolution_parameter /= igraph_vector_sum(node_weights); } /* Run actual Leiden algorithm for several iterations. */ if (!error) { - if (n_iterations > 0) { + if (n_iterations >= 0) { for (i = 0; !error && i < n_iterations; i++) { error = igraph_community_leiden(&self->g, edge_weights, node_weights, @@ -11839,13 +11839,13 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, } } else { while (!error && prev_quality < quality) { + prev_quality = quality; error = igraph_community_leiden(&self->g, edge_weights, node_weights, resolution_parameter, beta, start, membership, &nb_clusters, &quality); start = 1; - prev_quality = quality; } } } @@ -15855,7 +15855,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " @param n_iterations: the number of iterations to iterate the Leiden\n" " algorithm. Each iteration may improve the partition further.\n" " @return: the community membership vector.\n" - }, + }, {"community_walktrap", (PyCFunction) igraphmodule_Graph_community_walktrap, METH_VARARGS | METH_KEYWORDS, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index f8db38ef8..a07c4473d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1422,27 +1422,30 @@ def k_core(self, *args): def community_leiden(self, objective_function="CPM", weights=None, resolution_parameter=1.0, beta=0.01, initial_membership=None, n_iterations=2, node_weights=None): - """community_leiden(objective_function=CPM, weights=None, + """community_leiden(objective_function=CPM, weights=None, resolution_parameter=1.0, beta=0.01, initial_membership=None, n_iterations=2, node_weights=None) Finds the community structure of the graph using the Leiden algorithm of Traag, van Eck & Waltman. - @keyword objective_function: whether to use the Constant Potts + @keyword objective_function: whether to use the Constant Potts Model (CPM) or modularity. Must be either C{"CPM"} or C{"modularity"}. @keyword weights: edge weights to be used. Can be a sequence or iterable or even an edge attribute name. @keyword resolution_parameter: the resolution parameter to use. - Higher resolutions lead to more smaller communities, while + Higher resolutions lead to more smaller communities, while lower resolutions lead to fewer larger communities. - @keyword beta: parameter affecting the randomness in the Leiden + @keyword beta: parameter affecting the randomness in the Leiden algorithm. This affects only the refinement step of the algorithm. @keyword initial_membership: if provided, the Leiden algorithm will try to improve this provided membership. If no argument is provided, the aglorithm simply starts from the singleton partition. @keyword n_iterations: the number of iterations to iterate the Leiden - algorithm. Each iteration may improve the partition further. + algorithm. Each iteration may improve the partition further. Using + a negative number of iterations will run until a stable iteration is + encountered (i.e. the quality was not increased during that + iteration). @keyword node_weights: the node weights used in the Leiden algorithm. If this is not provided, it will be automatically determined on the basis of whether you want to use CPM or modularity. If you do provide @@ -1451,14 +1454,14 @@ def community_leiden(self, objective_function="CPM", weights=None, @newfield ref: Reference @ref: Traag, V. A., Waltman, L., & van Eck, N. J. (2019). From Louvain - to Leiden: guaranteeing well-connected communities. Scientific + to Leiden: guaranteeing well-connected communities. Scientific reports, 9(1), 5233. doi: 10.1038/s41598-019-41695-z """ if objective_function.lower() not in ("cpm", "modularity"): raise ValueError("objective_function must be \"CPM\" or \"modularity\".") membership = GraphBase.community_leiden(self, - edge_weights=weights, node_weights=node_weights, + edge_weights=weights, node_weights=node_weights, resolution_parameter=resolution_parameter, normalize_resolution=(objective_function == "modularity"), beta=beta, initial_membership=initial_membership, n_iterations=n_iterations) @@ -3992,7 +3995,7 @@ def _ensure_set(value): # TODO(ntamas): some keyword arguments should be prioritized over # others; for instance, we have optimized code paths for _source and # _target in directed and undirected graphs if es.is_all() is True; - # these should be executed first. This matters only if there are + # these should be executed first. This matters only if there are # multiple keyword arguments and es.is_all() is True. for keyword, value in kwds.iteritems(): diff --git a/vendor/source/igraph b/vendor/source/igraph index 20ad88e75..48b1c706b 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 20ad88e759205e73d79f612663153ba97c56a866 +Subproject commit 48b1c706b4364c40678a3e01fde1766380dcfabd From 22df25dabd171c8e4c7286cfa21ef924f3894bc3 Mon Sep 17 00:00:00 2001 From: Puneetha Pai <21996583+PuneethaPai@users.noreply.github.com> Date: Mon, 29 Jun 2020 18:03:42 +0530 Subject: [PATCH 0015/1681] Weighted incidence (#303) --- src/igraph/__init__.py | 25 +++++++++++++ tests/test_bipartite.py | 80 ++++++++++++++++++++++++++++++++++------- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index a07c4473d..1991105d6 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2817,12 +2817,37 @@ def Incidence(klass, *args, **kwds): what the value is. If C{True}, non-zero entries are rounded up to the nearest integer and this will be the number of multiple edges created. + @param weighted: defines whether to create a weighted graph from the + incidence matrix. If it is c{None} then an unweighted graph is created + and the multiple argument is used to determine the edges of the graph. + If it is a string then for every non-zero matrix entry, an edge is created + and the value of the entry is added as an edge attribute named by the + weighted argument. If it is C{True} then a weighted graph is created and + the name of the edge attribute will be ‘weight’. + + @raise ValueError: if the weighted and multiple are passed together. @return: the graph with a binary vertex attribute named C{"type"} that stores the vertex classes. """ + weighted = kwds.pop("weighted", False) + is_weighted = True if weighted or weighted == "" else False + multiple = kwds.get("multiple", False) + if is_weighted and multiple: + raise ValueError("arguments weighted and multiple can not co-exist") result, types = klass._Incidence(*args, **kwds) result.vs["type"] = types + if is_weighted: + weight_attr = "weight" if weighted == True else weighted + mat = args[0] + _, rows, columns = result.get_incidence() + num_vertices_of_first_kind = len(rows) + for edge in result.es: + source, target = edge.tuple + if source in rows: + edge[weight_attr] = mat[source][target - num_vertices_of_first_kind] + else: + edge[weight_attr] = mat[target][source - num_vertices_of_first_kind] return result def bipartite_projection(self, types="type", multiplicity=True, probe1=-1, diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py index 7ce1e0901..74a57a002 100644 --- a/tests/test_bipartite.py +++ b/tests/test_bipartite.py @@ -34,24 +34,80 @@ def testFullBipartite(self): def testIncidence(self): g = Graph.Incidence([[0, 1, 1], [1, 2, 0]]) - self.assertTrue(g.vcount() == 5 and g.ecount() == 4 and g.is_directed() == False) - self.assertTrue(g.vs["type"] == [False]*2 + [True]*3) - self.assertTrue(sorted(g.get_edgelist()) == [(0,3),(0,4),(1,2),(1,3)]) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(sorted(g.get_edgelist()), [(0,3),(0,4),(1,2),(1,3)]) g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], multiple=True) - self.assertTrue(g.vcount() == 5 and g.ecount() == 5 and g.is_directed() == False) - self.assertTrue(g.vs["type"] == [False]*2 + [True]*3) - self.assertTrue(sorted(g.get_edgelist()) == [(0,3),(0,4),(1,2),(1,3),(1,3)]) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 5, not g.is_directed()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(sorted(g.get_edgelist()), [(0,3),(0,4),(1,2),(1,3),(1,3)]) g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True) - self.assertTrue(g.vcount() == 5 and g.ecount() == 4 and g.is_directed() == True) - self.assertTrue(g.vs["type"] == [False]*2 + [True]*3) - self.assertTrue(sorted(g.get_edgelist()) == [(0,3),(0,4),(1,2),(1,3)]) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(sorted(g.get_edgelist()), [(0,3),(0,4),(1,2),(1,3)]) g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, mode="in") - self.assertTrue(g.vcount() == 5 and g.ecount() == 4 and g.is_directed() == True) - self.assertTrue(g.vs["type"] == [False]*2 + [True]*3) - self.assertTrue(sorted(g.get_edgelist()) == [(2,1),(3,0),(3,1),(4,0)]) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(sorted(g.get_edgelist()), [(2,1),(3,0),(3,1),(4,0)]) + + # Create a weighted Graph + g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], weighted=True) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed(), g.is_weighted()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + + # Graph is not weighted when weighted=`str` + g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], weighted="some_attr_name") + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed(), not g.is_weighted()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(g.es["some_attr_name"], [1, 1, 1, 2]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + + # Graph is not weighted when weighted="" + g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], weighted="") + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed(), not g.is_weighted()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(g.es[""], [1, 1, 1, 2]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + + # Should work when directed=True and mode=out with weighted + g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, weighted=True) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + + # Should work when directed=True and mode=in with weighted + g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, mode="in", weighted=True) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) + self.assertListEqual(sorted(g.get_edgelist()), [(2, 1), (3, 0), (3, 1), (4, 0)]) + + # Should work when directed=True and mode=all with weighted + g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, mode="all", weighted=True) + self.assertTrue(all((g.vcount() == 5, g.ecount() == 8, g.is_directed(), g.is_weighted()))) + self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 1, 1, 2, 2]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3), (2, 1), (3, 0), (3, 1), (4, 0)]) + + def testIncidenceError(self): + msg = "arguments weighted and multiple can not co-exist" + with self.assertRaises(ValueError) as e: + Graph.Incidence([[0, 1, 1], [1, 2, 0]], multiple=True, weighted=True) + self.assertIn(msg, e.exception.args) + + with self.assertRaises(ValueError) as e: + Graph.Incidence([[0, 1, 1], [1, 2, 0]], multiple=True, weighted="string") + self.assertIn(msg, e.exception.args) + + with self.assertRaises(ValueError) as e: + Graph.Incidence([[0, 1, 1], [1, 2, 0]], multiple=True, weighted="") + self.assertIn(msg, e.exception.args) def testGetIncidence(self): mat = [[0, 1, 1], [1, 1, 0]] From c94925d1cfa1a1d910f6ab65a5966f05f7578069 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 14 Jul 2020 14:44:00 +0200 Subject: [PATCH 0016/1681] update vendored igraph to 0.8.2 to fix MSVC compilation error --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 48b1c706b..4986fba9e 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 48b1c706b4364c40678a3e01fde1766380dcfabd +Subproject commit 4986fba9e43740acf2f02d081aea8dd221e2372c From afe2230bd7a0c2606ddab582e0804e2542404b48 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 14 Jul 2020 23:03:17 +1000 Subject: [PATCH 0017/1681] Conversion of objects to/from other libraries (#242) --- src/igraph/__init__.py | 176 ++++++++++++++ src/igraph/test/foreign.py | 480 +++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 3 files changed, 657 insertions(+) create mode 100644 src/igraph/test/foreign.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 1991105d6..56938a1f1 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1742,6 +1742,182 @@ def maximum_bipartite_matching(self, types="type", weights=None, eps=None): ############################################# # Auxiliary I/O functions + def to_networkx(self): + """Converts the graph to networkx format""" + import networkx as nx + + # Graph: decide on directness and mutliplicity + if any(self.is_multiple()): + if self.is_directed(): + klass = nx.MultiDiGraph + else: + klass = nx.MultiGraph + else: + if self.is_directed(): + klass = nx.DiGraph + else: + klass = nx.Graph + + # Graph attributes + kw = {x: self[x] for x in self.attributes()} + g = klass(**kw) + + # Nodes and node attributes + for i, v in enumerate(self.vs): + g.add_node(i, **v.attributes()) + + # Edges and edge attributes + for edge in self.es: + g.add_edge(edge.source, edge.target, **edge.attributes()) + + return g + + @classmethod + def from_networkx(klass, g): + """Converts the graph from networkx + + Vertex names will be converted to "_nx_name" attribute and the vertices + will get new ids from 0 up (as standard in igraph). + + @param g: networkx Graph or DiGraph + """ + import networkx as nx + + # Graph attributes + gattr = dict(g.graph) + + # Nodes + vnames = list(g.nodes) + vattr = {'_nx_name': vnames} + vcount = len(vnames) + vd = {v: i for i, v in enumerate(vnames)} + + # NOTE: we do not need a special class for multigraphs, it is taken + # care for at the edge level rather than at the graph level. + graph = klass( + n=vcount, + directed=g.is_directed(), + graph_attrs=gattr, + vertex_attrs=vattr) + + # Node attributes + for v, datum in g.nodes.data(): + for key, val in datum.items(): + graph.vs[vd[v]][key] = val + + # Edges and edge attributes + # NOTE: we need to do both together to deal well with multigraphs + # Each e might have a length of 2 (graphs) or 3 (multigraphs, the + # third element is the "color" of the edge) + for e, (_, _, datum) in zip(g.edges, g.edges.data()): + eid = graph.add_edge(vd[e[0]], vd[e[1]]) + for key, val in datum.items(): + eid[key] = val + + return graph + + def to_graph_tool( + self, + graph_attributes=None, + vertex_attributes=None, + edge_attributes=None): + """Converts the graph to graph-tool + + @param graph_attributes: dictionary of graph attributes to transfer. + Keys are attributes from the graph, values are data types (see + below). C{None} means no graph attributes are transferred. + @param vertex_attributes: dictionary of vertex attributes to transfer. + Keys are attributes from the vertices, values are data types (see + below). C{None} means no vertex attributes are transferred. + @param edge_attributes: dictionary of edge attributes to transfer. + Keys are attributes from the edges, values are data types (see + below). C{None} means no vertex attributes are transferred. + + Data types: graph-tool only accepts specific data types. See the + following web page for a list: + + https://graph-tool.skewed.de/static/doc/quickstart.html + + NOTE: because of the restricted data types in graph-tool, vertex and + edge attributes require to be type-consistent across all vertices or + edges. If you set the property for only some vertices/edges, the other + will be tagged as None in python-igraph, so they can only be converted + to graph-tool with the type 'object' and any other conversion will + fail. + """ + import graph_tool as gt + + # Graph + g = gt.Graph(directed=self.is_directed()) + + # Nodes + vc = self.vcount() + g.add_vertex(vc) + + # Graph attributes + if graph_attributes is not None: + for x, dtype in graph_attributes.items(): + # Strange syntax for setting internal properties + gprop = g.new_graph_property(str(dtype)) + g.graph_properties[x] = gprop + g.graph_properties[x] = self[x] + + # Vertex attributes + if vertex_attributes is not None: + for x, dtype in vertex_attributes.items(): + # Create a new vertex property + g.vertex_properties[x] = g.new_vertex_property(str(dtype)) + # Fill the values from the igraph.Graph + for i in range(vc): + g.vertex_properties[x][g.vertex(i)] = self.vs[i][x] + + # Edges and edge attributes + if edge_attributes is not None: + for x, dtype in edge_attributes.items(): + g.edge_properties[x] = g.new_edge_property(str(dtype)) + for edge in self.es: + e = g.add_edge(edge.source, edge.target) + if edge_attributes is not None: + for x, dtype in edge_attributes.items(): + prop = edge.attributes().get(x, None) + g.edge_properties[x][e] = prop + + return g + + @classmethod + def from_graph_tool(klass, g): + """Converts the graph from graph-tool + + @param g: graph-tool Graph + """ + # Graph attributes + gattr = dict(g.graph_properties) + + # Nodes + vcount = g.num_vertices() + + # Graph + graph = klass( + n=vcount, + directed=g.is_directed(), + graph_attrs=gattr) + + # Node attributes + for key, val in g.vertex_properties.items(): + prop = val.get_array() + for i in range(vcount): + graph.vs[i][key] = prop[i] + + # Edges + # NOTE: the order the edges are put in is necessary to set the + # attributes later on + for e in g.edges(): + edge = graph.add_edge(int(e.source()), int(e.target())) + for key, val in g.edge_properties.items(): + edge[key] = val[e] + + return graph + def write_adjacency(self, f, sep=" ", eol="\n", *args, **kwds): """Writes the adjacency matrix of the graph to the given file diff --git a/src/igraph/test/foreign.py b/src/igraph/test/foreign.py new file mode 100644 index 000000000..af8c1c950 --- /dev/null +++ b/src/igraph/test/foreign.py @@ -0,0 +1,480 @@ +from __future__ import with_statement + +import io +import unittest +import warnings + +from igraph import * +from igraph.test.utils import temporary_file + + +try: + import networkx as nx +except ImportError: + nx = None + + +try: + import graph_tool as gt +except ImportError: + gt = None + + +class ForeignTests(unittest.TestCase): + def testDIMACS(self): + with temporary_file(u"""\ + c + c This is a simple example file to demonstrate the + c DIMACS input file format for minimum-cost flow problems. + c + c problem line : + p max 4 5 + c + c node descriptor lines : + n 1 s + n 4 t + c + c arc descriptor lines : + a 1 2 4 + a 1 3 2 + a 2 3 2 + a 2 4 3 + a 3 4 5 + """) as tmpfname: + graph = Graph.Read_DIMACS(tmpfname, False) + self.assertTrue(isinstance(graph, Graph)) + self.assertTrue(graph.vcount() == 4 and graph.ecount() == 5) + self.assertTrue(graph["source"] == 0 and graph["target"] == 3) + self.assertTrue(graph.es["capacity"] == [4,2,2,3,5]) + graph.write_dimacs(tmpfname) + + + def testDL(self): + with temporary_file(u"""\ + dl n=5 + format = fullmatrix + labels embedded + data: + larry david lin pat russ + Larry 0 1 1 1 0 + david 1 0 0 0 1 + Lin 1 0 0 1 0 + Pat 1 0 1 0 1 + russ 0 1 0 1 0 + """) as tmpfname: + g = Graph.Read_DL(tmpfname) + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 5 and g.ecount() == 12) + self.assertTrue(g.is_directed()) + self.assertTrue(sorted(g.get_edgelist()) == \ + [(0,1),(0,2),(0,3),(1,0),(1,4),(2,0),(2,3),(3,0),\ + (3,2),(3,4),(4,1),(4,3)]) + + with temporary_file(u"""\ + dl n=5 + format = fullmatrix + labels: + barry,david + lin,pat + russ + data: + 0 1 1 1 0 + 1 0 0 0 1 + 1 0 0 1 0 + 1 0 1 0 1 + 0 1 0 1 0 + """) as tmpfname: + g = Graph.Read_DL(tmpfname) + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 5 and g.ecount() == 12) + self.assertTrue(g.is_directed()) + self.assertTrue(sorted(g.get_edgelist()) == \ + [(0,1),(0,2),(0,3),(1,0),(1,4),(2,0),(2,3),(3,0),\ + (3,2),(3,4),(4,1),(4,3)]) + + with temporary_file(u"""\ + DL n=5 + format = edgelist1 + labels: + george, sally, jim, billy, jane + labels embedded: + data: + george sally 2 + george jim 3 + sally jim 4 + billy george 5 + jane jim 6 + """) as tmpfname: + g = Graph.Read_DL(tmpfname, False) + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 5 and g.ecount() == 5) + self.assertTrue(not g.is_directed()) + self.assertTrue(sorted(g.get_edgelist()) == \ + [(0,1),(0,2),(0,3),(1,2),(2,4)]) + + def _testNCOLOrLGL(self, func, fname, can_be_reopened=True): + g = func(fname, names=False, weights=False, \ + directed=False) + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 4 and g.ecount() == 5) + self.assertTrue(not g.is_directed()) + self.assertTrue(sorted(g.get_edgelist()) == \ + [(0,1),(0,2),(1,1),(1,3),(2,3)]) + self.assertTrue("name" not in g.vertex_attributes() and \ + "weight" not in g.edge_attributes()) + if not can_be_reopened: + return + + g = func(fname, names=False, \ + directed=False) + self.assertTrue("name" not in g.vertex_attributes() and \ + "weight" in g.edge_attributes()) + self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) + + g = func(fname, directed=False) + self.assertTrue("name" in g.vertex_attributes() and \ + "weight" in g.edge_attributes()) + self.assertTrue(g.vs["name"] == ["eggs", "spam", "ham", "bacon"]) + self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) + + def testNCOL(self): + with temporary_file(u"""\ + eggs spam 1 + ham eggs 2 + ham bacon + bacon spam 3 + spam spam""") as tmpfname: + self._testNCOLOrLGL(func=Graph.Read_Ncol, fname=tmpfname) + + with temporary_file(u"""\ + eggs spam + ham eggs + ham bacon + bacon spam + spam spam""") as tmpfname: + g = Graph.Read_Ncol(tmpfname) + self.assertTrue("name" in g.vertex_attributes() and \ + "weight" not in g.edge_attributes()) + + def testLGL(self): + with temporary_file(u"""\ + # eggs + spam 1 + # ham + eggs 2 + bacon + # bacon + spam 3 + # spam + spam""") as tmpfname: + self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=tmpfname) + + with temporary_file(u"""\ + # eggs + spam + # ham + eggs + bacon + # bacon + spam + # spam + spam""") as tmpfname: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + g = Graph.Read_Lgl(tmpfname) + self.assertTrue("name" in g.vertex_attributes() and \ + "weight" not in g.edge_attributes()) + + # This is not an LGL file; we are testing error handling here + with temporary_file(u"""\ + 1 2 + 1 3 + """) as tmpfname: + with self.assertRaises(InternalError): + Graph.Read_Lgl(tmpfname) + + def testLGLWithIOModule(self): + with temporary_file(u"""\ + # eggs + spam 1 + # ham + eggs 2 + bacon + # bacon + spam 3 + # spam + spam""") as tmpfname: + with io.open(tmpfname, "r") as fp: + self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=fp, + can_be_reopened=False) + + def testAdjacency(self): + with temporary_file(u"""\ + # Test comment line + 0 1 1 0 0 0 + 1 0 1 0 0 0 + 1 1 0 0 0 0 + 0 0 0 0 2 2 + 0 0 0 2 0 2 + 0 0 0 2 2 0 + """) as tmpfname: + g = Graph.Read_Adjacency(tmpfname) + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 6 and g.ecount() == 18 and + g.is_directed() and "weight" not in g.edge_attributes()) + g = Graph.Read_Adjacency(tmpfname, attribute="weight") + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 6 and g.ecount() == 12 and + g.is_directed() and g.es["weight"] == [1,1,1,1,1,1,2,2,2,2,2,2]) + + g.write_adjacency(tmpfname) + + def testPickle(self): + pickle = [128, 2, 99, 105, 103, 114, 97, 112, 104, 10, 71, 114, 97, 112, + 104, 10, 113, 1, 40, 75, 3, 93, 113, 2, 75, 1, 75, 2, 134, 113, 3, 97, + 137, 125, 125, 125, 116, 82, 113, 4, 125, 98, 46] + if sys.version_info > (3, 0): + pickle = bytes(pickle) + else: + pickle = "".join(map(chr, pickle)) + with temporary_file(pickle, "wb") as tmpfname: + g = Graph.Read_Pickle(pickle) + self.assertTrue(isinstance(g, Graph)) + self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and + not g.is_directed()) + g.write_pickle(tmpfname) + + @unittest.skipIf(nx is None, "test case depends on networkx") + def testGraphNetworkx(self): + # Undirected + g = Graph.Ring(10) + g['gattr'] = 'graph_attribute' + g.vs['vattr'] = list(range(g.vcount())) + g.es['eattr'] = list(range(len(g.es))) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertFalse(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual( + sorted(['vattr', '_nx_name']), + sorted(g2.vertex_attributes())) + for i, vertex in enumerate(g.vs): + vertex2 = g2.vs[i] + for an in vertex.attribute_names(): + if an == 'vattr': + continue + self.assertEqual( + vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + for edge in g.es: + eid = g2.get_eid(edge.source, edge.target) + edge2 = g2.es[eid] + for an in edge.attribute_names(): + self.assertEqual(edge.attributes()[an], edge2.attributes()[an]) + + # Directed + g = Graph.Ring(10, directed=True) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertTrue(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + @unittest.skipIf(nx is None, "test case depends on networkx") + def testMultigraphNetworkx(self): + # Undirected + g = Graph.Ring(10) + g.add_edge(0, 1) + g['gattr'] = 'graph_attribute' + g.vs['vattr'] = list(range(g.vcount())) + g.es['eattr'] = list(range(len(g.es))) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertFalse(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual( + sorted(['vattr', '_nx_name']), + sorted(g2.vertex_attributes())) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + # Testing parallel edges is a bit more tricky + edge2_found = set() + for edge in g.es: + # Go through all parallel edges between these two vertices + for edge2 in g2.es: + if edge2 in edge2_found: + continue + if edge.source != edge2.source: + continue + if edge.target != edge2.target: + continue + # Check all attributes between these two + for an in edge.attribute_names(): + if edge.attributes()[an] != edge2.attributes()[an]: + break + else: + # Correspondence found + edge2_found.add(edge2) + break + + else: + self.assertTrue(False) + + # Directed + g = Graph.Ring(10, directed=True) + g.add_edge(0, 1) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertTrue(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + @unittest.skipIf(gt is None, "test case depends on graph-tool") + def testGraphGraphTool(self): + # Undirected + g = Graph.Ring(10) + g['gattr'] = 'graph_attribute' + g.vs['vattr'] = list(range(g.vcount())) + g.es['eattr'] = list(range(len(g.es))) + + # Go to graph-tool and back + g_gt = g.to_graph_tool( + graph_attributes={'gattr': 'object'}, + vertex_attributes={'vattr': 'int'}, + edge_attributes={'eattr': 'int'}) + g2 = Graph.from_graph_tool(g_gt) + + self.assertFalse(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual(g.vertex_attributes(), g2.vertex_attributes()) + for i, vertex in enumerate(g.vs): + vertex2 = g2.vs[i] + for an in vertex.attribute_names(): + self.assertEqual( + vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + for edge in g.es: + eid = g2.get_eid(edge.source, edge.target) + edge2 = g2.es[eid] + for an in edge.attribute_names(): + self.assertEqual(edge.attributes()[an], edge2.attributes()[an]) + + # Directed + g = Graph.Ring(10, directed=True) + + # Go to graph-tool and back + g_gt = g.to_graph_tool() + + g2 = Graph.from_graph_tool(g_gt) + + self.assertTrue(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + @unittest.skipIf(gt is None, "test case depends on graph-tool") + def testMultigraphGraphTool(self): + # Undirected + g = Graph.Ring(10) + g.add_edge(0, 1) + g['gattr'] = 'graph_attribute' + g.vs['vattr'] = list(range(g.vcount())) + g.es['eattr'] = list(range(len(g.es))) + + # Go to graph-tool and back + g_gt = g.to_graph_tool( + graph_attributes={'gattr': 'object'}, + vertex_attributes={'vattr': 'int'}, + edge_attributes={'eattr': 'int'}) + g2 = Graph.from_graph_tool(g_gt) + + self.assertFalse(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual(g.vertex_attributes(), g2.vertex_attributes()) + for i, vertex in enumerate(g.vs): + vertex2 = g2.vs[i] + for an in vertex.attribute_names(): + self.assertEqual( + vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + # Testing parallel edges is a bit more tricky + edge2_found = set() + for edge in g.es: + # Go through all parallel edges between these two vertices + for edge2 in g2.es: + if edge2 in edge2_found: + continue + if edge.source != edge2.source: + continue + if edge.target != edge2.target: + continue + # Check all attributes between these two + for an in edge.attribute_names(): + if edge.attributes()[an] != edge2.attributes()[an]: + break + else: + # Correspondence found + edge2_found.add(edge2) + break + + else: + self.assertTrue(False) + + # Directed + g = Graph.Ring(10, directed=True) + g.add_edge(0, 1) + + # Go to graph-tool and back + g_gt = g.to_graph_tool() + g2 = Graph.from_graph_tool(g_gt) + + self.assertTrue(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + +def suite(): + foreign_suite = unittest.makeSuite(ForeignTests) + return unittest.TestSuite([foreign_suite]) + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + +if __name__ == "__main__": + test() + diff --git a/tox.ini b/tox.ini index d2c11f3e3..4806db4af 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ commands = python -m unittest deps = scipy; platform_python_implementation != "PyPy" numpy; platform_python_implementation != "PyPy" + networkx setenv = TESTING_IN_TOX=1 From 0ab5d0bb2bca4ea1468b7871b7d014a9a4587236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20K=C3=B6pcke?= Date: Tue, 14 Jul 2020 17:14:31 +0200 Subject: [PATCH 0018/1681] setup.py: Support MinGW Windows build (#297) --- setup.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 24806c863..75f644cc2 100644 --- a/setup.py +++ b/setup.py @@ -26,6 +26,7 @@ import shutil import subprocess import sys +import sysconfig from select import select @@ -38,6 +39,11 @@ ########################################################################### +def building_on_windows_msvc(): + """Returns True when using the non-MingW CPython interpreter on Windows""" + return platform.system() == "Windows" and sysconfig.get_platform() != "mingw" + + def create_dir_unless_exists(*args): """Creates a directory unless it exists already.""" path = os.path.join(*args) @@ -308,7 +314,7 @@ def compile_in(self, build_folder, source_folder=None): if retcode: return False - building_on_windows = platform.system() == "Windows" + building_on_windows = building_on_windows_msvc() if building_on_windows: print("Creating Microsoft Visual Studio project...") @@ -361,7 +367,7 @@ def compile_in(self, build_folder, source_folder=None): def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries ): - building_on_windows = platform.system() == "Windows" + building_on_windows = building_on_windows_msvc() create_dir_unless_exists(install_folder) @@ -691,7 +697,7 @@ def process_args_from_command_line(self): def replace_static_libraries(self, only=None, exclusions=None): """Replaces references to libraries with full paths to their static versions if the static version is to be found on the library path.""" - building_on_windows = platform.system() == "Windows" + building_on_windows = building_on_windows_msvc() if not building_on_windows and "stdc++" not in self.libraries: self.libraries.append("stdc++") @@ -711,7 +717,7 @@ def replace_static_libraries(self, only=None, exclusions=None): def use_vendored_igraph(self): """Assumes that igraph is installed already in ``vendor/install/igraph`` and sets up the include and library paths and the library names accordingly.""" - building_on_windows = platform.system() == "Windows" + building_on_windows = building_on_windows_msvc() buildcfg.include_dirs = [os.path.join("vendor", "install", "igraph", "include")] buildcfg.library_dirs = [os.path.join("vendor", "install", "igraph", "lib")] From 9ad245de5a3fa38bd31fc004c69bf1664a1b6740 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 14 Jul 2020 17:15:13 +0200 Subject: [PATCH 0019/1681] typo fix --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 75f644cc2..8aa98e59d 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def building_on_windows_msvc(): - """Returns True when using the non-MingW CPython interpreter on Windows""" + """Returns True when using the non-MinGW CPython interpreter on Windows""" return platform.system() == "Windows" and sysconfig.get_platform() != "mingw" From feb9d4b6ed832d9a1023e942b51b7d775cbbcbb6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 26 Jul 2020 11:21:04 +0200 Subject: [PATCH 0020/1681] document that canonical_permutation() supports colors, refs #311 --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index dac3b4740..43bb56614 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14825,7 +14825,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"canonical_permutation", (PyCFunction) igraphmodule_Graph_canonical_permutation, METH_VARARGS | METH_KEYWORDS, - "canonical_permutation(sh=\"fm\")\n\n" + "canonical_permutation(sh=\"fm\", color=None)\n\n" "Calculates the canonical permutation of a graph using the BLISS isomorphism\n" "algorithm.\n\n" "Passing the permutation returned here to L{Graph.permute_vertices()} will\n" From 3e4fa50433a311910fd32bb721316d7f2f85b3d1 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 27 Jul 2020 18:08:17 +1000 Subject: [PATCH 0021/1681] Loading graph from one or two dataframes (#309) Co-authored-by: Tamas Nepusz --- src/igraph/__init__.py | 79 ++++++++++++++++++++++++++++++++++++++++ tests/test_generators.py | 34 ++++++++++++++++- 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 56938a1f1..876fa143f 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3026,6 +3026,85 @@ def Incidence(klass, *args, **kwds): edge[weight_attr] = mat[target][source - num_vertices_of_first_kind] return result + @classmethod + def DataFrame(klass, edges, directed=True, vertices=None): + """DataFrame(directed=True, vertices=None) + + Generates a graph from one or two dataframes. + + @param edges: pandas DataFrame containing edges and metadata + @param directed: bool setting whether the graph is directed + @param vertices: None (default) or pandas DataFrame containing vertex + metadata. The first column must contain the unique ids of the + vertices and will be set as attribute 'name'. All other columns + will be added as vertex attributes by column name. + + @return: the graph + """ + import numpy as np + import pandas as pd + + if edges.shape[1] < 2: + raise ValueError("the data frame should contain at least two columns") + + # Handle if some elements are 'NA' + if edges.iloc[:, :2].isna().values.any(): + warn("In 'edges' NA elements were replaced with string \"NA\"") + edges = edges.copy() + edges.iloc[:, :2].fillna('NA', inplace=True) + + if (vertices is not None) and vertices.iloc[:, 0].isna().values.any(): + warn("In the first column of 'vertices' NA elements were replaced "+ + "with string \"NA\"") + vertices = vertices.copy() + vertices.iloc[:, 0].fillna('NA', inplace=True) + + names = np.unique(edges.values[:, :2]) + + if vertices is not None: + names_edges = names + if vertices.shape[1] < 1: + raise ValueError('vertices has no columns') + + names = vertices.iloc[:, 0].astype(str) + + if names.duplicated().any(): + raise ValueError('Vertex names must be unique') + + if len(np.setdiff1d(names_edges, names.values)): + raise ValueError( + 'Some vertices in the edge DataFrame are missing from vertices DataFrame') + + names = names.values + + # create graph + g = Graph(n=len(names), directed=directed) + + # vertex attributes + if vertices is not None: + cols = vertices.columns + for v, (_, attr) in zip(g.vs, vertices.iterrows()): + v['name'] = attr[cols[0]] + if len(cols) > 1: + for an in cols[1:]: + v[an] = attr[an] + + # create edge list + names_idx = pd.Series(index=names, data=np.arange(len(names))) + e0 = names_idx[edges.values[:, 0]] + e1 = names_idx[edges.values[:, 1]] + + # add the edges + g.add_edges(zip(e0, e1)) + + # edge attributes + if edges.shape[1] > 2: + for e, (_, attr) in zip(g.es, edges.iloc[:, 2:]): + for an, av in attr.items(): + e[an] = av + + return g + def bipartite_projection(self, types="type", multiplicity=True, probe1=-1, which="both"): """Projects a bipartite graph into two one-mode graphs. Edge directions diff --git a/tests/test_generators.py b/tests/test_generators.py index ddb64222e..a8e20d403 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,6 +1,14 @@ import unittest from igraph import * +try: + import numpy as np + import pandas as pd +except ImportError: + np = None + pd = None + + class GeneratorTests(unittest.TestCase): def testStar(self): g=Graph.Star(5, "in") @@ -164,7 +172,31 @@ def testWeightedAdjacency(self): self.assertTrue(el == [(0,1), (0,2), (1,0), (3,1)]) self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) - + @unittest.skipIf((np is None) or (pd is None), "test case depends on NumPy/Pandas") + def testDataFrame(self): + edges = pd.DataFrame( + [['C', 'A', 0.4], ['A', 'B', 0.1]], + columns=[0, 1, 'weight']) + + g = Graph.DataFrame(edges, directed=False) + self.assertTrue(g.es["weight"] == [0.1, 0.4]) + + vertices = pd.DataFrame( + [['A', 'blue'], ['B', 'yellow'], ['C', 'blue']], + columns=[0, 'color']) + + g = Graph.DataFrame(edges, directed=True, vertices=vertices) + self.assertTrue(g.vs['name'] == ['A', 'B', 'C']) + self.assertTrue(g.vs["color"] == ['blue', 'yellow', 'blue']) + self.assertTrue(g.es["weight"] == [0.4, 0.1]) + + vertices.iloc[0, 0] = np.nan + g = Graph.DataFrame(edges, directed=True, vertices=vertices) + self.assertTrue(g.vs['name'] == ['NA', 'B', 'C']) + self.assertTrue(g.vs["color"] == ['blue', 'yellow', 'blue']) + self.assertTrue(g.es["weight"] == [0.4, 0.1]) + + def suite(): generator_suite = unittest.makeSuite(GeneratorTests) return unittest.TestSuite([generator_suite]) From 316fa6b3b42e348561b8b199338422841aceeb3c Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 27 Jul 2020 18:13:10 +1000 Subject: [PATCH 0022/1681] Documenting graph generation (#308) --- doc/source/generation.rst | 165 +++++++++++++++++++++++++++++++++++++- src/_igraph/graphobject.c | 16 ++-- tests/test_isomorphism.py | 2 +- 3 files changed, 173 insertions(+), 10 deletions(-) diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 475b50e56..3c42c39e4 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -1,5 +1,168 @@ Graph generation ================ -.. note:: TODO. This is a placeholder section; it is not written yet. +The first step of most |igraph| applications is to generate a graph. This section will explain a number of ways to do that. Read the `API documentation`_ for details on each function and class. +The :class:`Graph` class is the main object used to generate graphs: + +>>> from igraph import Graph + +From nodes and edges +++++++++++++++++++++ +Nodes are always numbered from 0 upwards. To create a generic graph with a specified number of nodes (e.g. 10) and a list of edges between them, you can use the generic constructor: + +>>> g = Graph(n=10, edges=[[0, 1], [2, 3]]) + +If not specified, the graph is undirected. To make a directed graph: + +>>> g = Graph(n=10, edges=[[0, 1], [2, 3]], directed=True) + +To specify edge weights (or any other vertex/edge attributes), use dictionaries: + +>>> g = Graph(n=4, edges=[[0, 1], [2, 3]], +>>> edge_attrs={'weight': [0.1, 0.2]}, +>>> vertex_attrs={'color': ['b', 'g', 'g', 'y']}) + +Variations on this constructor is :meth:`Graph.DictList`, which constructs a graph from a list-of-dictionaries representation, and :meth:`Graph.TupleList`, which constructs a graph from a list-of-tuples representation. + +To create a bipartite graph from a list of types and a list of edges, use :meth:`Graph.Bipartite`. + +From matrices ++++++++++++++ +To create a graph from an adjecency matrix, use :meth:`Graph.Adjacency` or, for weighted matrices, :meth:`Graph.Weighted_Adjacency`: + +>>> g = Graph.Adjacency([[0, 1, 1], [0, 0, 0], [0, 0, 1]]) + +This graph is directed and has edges `[0, 1]`, `[0, 2]` and `[2, 2]` (a loop). + +To create a bipartite graph from an incidence matrix, use :meth:`Graph.Incidence`: + +>>> g = Graph.Incidence([[0, 1, 1], [1, 1, 0]]) + +From file ++++++++++ +To load a graph from a preexisting file in any of the supported formats, use :meth:`Graph.Load`. For instance: + +>>> g = Graph.Load('myfile.gml', format='gml') + +If you don't specify a format, |igraph| will try to figure it out or, if that fails, it will complain. + +From external libraries ++++++++++++++++++++++++ +|igraph| can read from and write to `networkx`_ and `graph-tool`_ graph formats: + +>>> g = Graph.from_networkx(nwx) + +and + +>>> g = Graph.from_graph_tool(gt) + +From pandas DataFrame(s) +++++++++++++++++++++++++ +A common practice is to store edges in a `pandas.DataFrame`, where the two first columns are the source and target vertex ids, +and any additional column indicates edge attributes. You can generate a graph via :meth:`Graph.DataFrame`: + +>>> g = Graph.DataFrame(edges, directed=False) + +It is possible to set vertex attributes at the same time via a separate DataFrame. The first column is assumed to contain all +vertex ids (including any vertices without edges) and any additional columns are vertex attributes: + +>>> g = Graph.DataFrame(edges, directed=False, vertices=vertices) + +From a formula +++++++++++++++ +To create a graph from a string formula, use :meth:`Graph.Formula`, e.g.: + +>>> g = Graph.Formula('D-A:B:F:G, A-C-F-A, B-E-G-B, A-B, F-G, H-F:G, H-I-J') + +.. note:: This particular formula also assigns the 'name' attribute to vertices. + +Full graphs ++++++++++++ +To create a full graph, use :meth:`Graph.Full`: + +>>> g = Graph.Full(n=3) + +where `n` is the number of nodes. You can specify directedness and whether self loops are allowed: + +>>> g = Graph.Full(n=3, directed=True, loops=True) + +A similar method, :meth:`Graph.Full_Bipartite`, generates a full bipartite graph. Finally, the metho :meth:`Graph.Full_Citation` created the full citation graph, in which each vertex `i` has a directed edge to all vertices strictly smaller than `i`. + +Tree and star ++++++++++++++ +:meth:`Graph.Tree` can be used to generate regular trees, in which almost each vertex has the same number of children: + +>>> g = Graph.Tree(n=7, n_children=2) + +creates a tree with seven vertices - of which four are leaves. The root (0) has two children (1 and 2), each of which has two children (the four leaves). Regular trees can be directed or undirected (default). + +The method :meth:`Graph.Star` creates a star graph, which is a subtype of a tree. + +Lattice ++++++++ +:meth:`Graph.Lattice` creates a regular lattice of the chosen size. For instance: + +>>> g = Graph.Lattice(dim=[3, 3], circular=False) + +creates a 3x3 grid in two dimensions (9 vertices total). `circular` is used to connect each edge of the lattice back onto the other side, a process also known as "periodic boundary condition" that is sometimes helpful to smoothen out edge effects. + +The one dimensional case (path graph or ring) is important enough to deserve its own constructor :meth:`Graph.Ring`, which can be circular or not: + +>>> g = Graph.Ring(n=4, circular=False) + +Graph atlas ++++++++++++ +The book ‘An Atlas of Graphs’ by Roland C. Read and Robin J. Wilson contains all undirected graphs with up to seven vertices, numbered from 0 up to 1252. You can create any graph from this list by index with :meth:`Graph.Atlas`, e.g.: + +>>> g = Graph.Atlas(44) + +The graphs are listed: + + - in increasing order of number of nodes; + - for a fixed number of nodes, in increasing order of the number of edges; + - for fixed numbers of nodes and edges, in increasing order of the degree sequence, for example 111223 < 112222; + - for fixed degree sequence, in increasing number of automorphisms. + + +Famous graphs ++++++++++++++ +A curated list of famous graphs, which are often used in the literature for benchmarking and other purposes, is available on the `igraph C core manual `_. You can generate any graph in that list by name, e.g.: + +>>> g = Graph.Famous('Zachary') + +will teach you some about martial arts. + + +Random graphs ++++++++++++++ +Stochastic graphs can be created according to several different models or games: + + - Barabasi-Albert model: :meth:`Graph.Barabasi` + - Erdos-Renyi: :meth:`Graph.Erdos_Renyi` + - Watts-Strogatz :meth:`Graph.Watts_Strogatz` + - stochastic block model :meth:`Graph.SBM` + - forest fire game :meth:`Graph.Forest_Fire` + - random geometric graph :meth:`Graph.GRG` + - growing :meth:`Graph.Growing_Random` + - establishment game :meth:`Graph.Establishment` + - preference, the non-growing variant of establishment :meth:`Graph.Preference` + - asymmetric preference :meth:`Graph.Asymmetric_Prefernce` + - recent degree :meth:`Graph.Recent_Degree` + - k-regular (each node has degree K) :meth:`Graph.K_Regular` + - non-growing graph with edge probabilities proportional to node fitnesses :meth:`Graph.Static_Fitness` + - non-growing graph with prescribed power-law degree distribution(s) :meth:`Graph.Static_Power_Law` + - random graph with a given degree sequence :meth:`Graph.Degree_Sequence` + - bipartite :meth:`Graph.Random_Bipartite` + + +Other graphs +++++++++++++ +Finally, there are some ways of generating graphs that are not covered by the previous sections: + + - Kautz graphs :meth:`Graph.Kautz` + - De Bruijn graphs :meth:`Graph.De_Bruijn` + - Lederberg-Coxeter-Frucht graphs :meth:`Graph.LCF` + - graphs with a specified isomorphism class :meth:`Graph.Isoclass` + +.. _API documentation: https://igraph.org/python/doc/igraph-module.html diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 43bb56614..89ae5ba56 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2573,7 +2573,7 @@ PyObject *igraphmodule_Graph_Incidence(PyTypeObject * type, } /** \ingroup python_interface_graph - * \brief Generates a graph with a given isomorphy class + * \brief Generates a graph with a given isomorphism class * This is intended to be a class method in Python, so the first argument * is the type object and not the Python igraph object (because we have * to allocate that in this method). @@ -8306,7 +8306,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( } /** \ingroup python_interface_graph - * \brief Calculates the isomorphy class of a graph or its subgraph + * \brief Calculates the isomorphism class of a graph or its subgraph * \sa igraph_isoclass, igraph_isoclass_subgraph */ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, @@ -12738,9 +12738,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"Isoclass", (PyCFunction) igraphmodule_Graph_Isoclass, METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Isoclass(n, class, directed=False)\n\n" - "Generates a graph with a given isomorphy class.\n\n" + "Generates a graph with a given isomorphism class.\n\n" "@param n: the number of vertices in the graph (3 or 4)\n" - "@param class: the isomorphy class\n" + "@param class: the isomorphism class\n" "@param directed: whether the graph should be directed.\n"}, /* interface to igraph_watts_strogatz_game */ @@ -14037,7 +14037,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " motifs.\n" "@param callback: C{None} or a callable that will be called for every motif\n" " found in the graph. The callable must accept three parameters: the graph\n" - " itself, the list of vertices in the motif and the isomorphy class of the\n" + " itself, the list of vertices in the motif and the isomorphism class of the\n" " motif (see L{Graph.isoclass()}). The search will stop when the callback\n" " returns an object with a non-zero truth value or raises an exception.\n" "@return: the list of motifs if I{callback} is C{None}, or C{None} otherwise\n" @@ -14853,13 +14853,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"isoclass", (PyCFunction) igraphmodule_Graph_isoclass, METH_VARARGS | METH_KEYWORDS, "isoclass(vertices)\n\n" - "Returns the isomorphy class of the graph or its subgraph.\n\n" + "Returns the isomorphism class of the graph or its subgraph.\n\n" "Isomorphy class calculations are implemented only for graphs with\n" "3 or 4 vertices.\n\n" "@param vertices: a list of vertices if we want to calculate the\n" - " isomorphy class for only a subset of vertices. C{None} means to\n" + " isomorphism class for only a subset of vertices. C{None} means to\n" " use the full graph.\n" - "@return: the isomorphy class of the (sub)graph\n\n"}, + "@return: the isomorphism class of the (sub)graph\n\n"}, {"isomorphic", (PyCFunction) igraphmodule_Graph_isomorphic, METH_VARARGS | METH_KEYWORDS, "isomorphic(other)\n\n" diff --git a/tests/test_isomorphism.py b/tests/test_isomorphism.py index 9b8fa7437..b8e6c0256 100644 --- a/tests/test_isomorphism.py +++ b/tests/test_isomorphism.py @@ -22,7 +22,7 @@ def testIsomorphic(self): (5, 1), (5, 4), (5, 6), \ (7, 3), (7, 6), (7, 4)]) - # Test the isomorphy of g1 and g2 + # Test the isomorphism of g1 and g2 self.assertTrue(g1.isomorphic(g2)) self.assertTrue(g2.isomorphic_vf2(g1, return_mapping_21=True) \ == (True, None, [0, 2, 5, 7, 1, 3, 4, 6])) From 5adcf61b75a483903c0a363bd52449b666b4e345 Mon Sep 17 00:00:00 2001 From: Christopher Falter Date: Tue, 28 Jul 2020 10:00:07 -0400 Subject: [PATCH 0023/1681] Update tutorial.rst to stop using deprecated syntax for Graph.degree (#313) --- doc/source/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index f95f4d7da..935029af9 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -369,7 +369,7 @@ the vertex) and :dfn:`out-degree` (the number of edges originating from the vert [3, 1, 4, 3, 2, 3, 2] If the graph was directed, we would have been able to calculate the in- and out-degrees -separately using ``g.degree(type="in")`` and ``g.degree(type="out")``. You can +separately using ``g.degree(mode="in")`` and ``g.degree(mode="out")``. You can also pass a single vertex ID or a list of vertex IDs to :meth:`~Graph.degree` if you want to calculate the degrees for only a subset of vertices: From 743ce690f7fcea94df72765190f623f348f7fad0 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 31 Jul 2020 21:58:13 +1000 Subject: [PATCH 0024/1681] Add vertices/edges with attributes (#319) --- src/igraph/__init__.py | 43 +++++++++++++++++++++++++++++++++++------- tests/test_basic.py | 18 ++++++++++++++++-- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 876fa143f..5f999b6a8 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -258,7 +258,7 @@ def add_edge(self, source, target, **kwds): edge[key] = value return edge - def add_edges(self, es): + def add_edges(self, es, attributes=None): """add_edges(es) Adds some edges to the graph. @@ -266,8 +266,17 @@ def add_edges(self, es): @param es: the list of edges to be added. Every edge is represented with a tuple containing the vertex IDs or names of the two endpoints. Vertices are enumerated from zero. + @params attributes: dict of sequences, all of length equal to the + number of edges to be added, containing the attributes of the new + edges. """ - return GraphBase.add_edges(self, es) + eid = self.ecount() + res = GraphBase.add_edges(self, es) + n = self.ecount() - eid + if (attributes is not None) and (n > 0): + for key, val in attributes.items(): + self.es[eid:][key] = val + return res def add_vertex(self, name=None, **kwds): """add_vertex(name=None, **kwds) @@ -291,7 +300,7 @@ def add_vertex(self, name=None, **kwds): vertex["name"] = name return vertex - def add_vertices(self, n): + def add_vertices(self, n, attributes=None): """add_vertices(n) Adds some vertices to the graph. @@ -300,13 +309,24 @@ def add_vertices(self, n): vertex to be added, or a sequence of strings, each corresponding to the name of a vertex to be added. Names will be assigned to the C{name} vertex attribute. + @params attributes: dict of sequences, all of length equal to the + number of vertices to be added, containing the attributes of the new + vertices. If n is a string (so a single vertex is added), then the + values of this dict are the attributes themselves, but if n=1 then + they have to be lists of length 1. + + Note that if n is a sequence of strings, indicating the names of the + new vertices, and attributes has a key 'name', the two conflict. In + that case the attribute will be applied. """ if isinstance(n, basestring): # Adding a single vertex with a name m = self.vcount() result = GraphBase.add_vertices(self, 1) self.vs[m]["name"] = n - return result + if attributes is not None: + for key, val in attributes.items(): + self.vs[m][key] = val elif hasattr(n, "__iter__"): m = self.vcount() if not hasattr(n, "__len__"): @@ -314,9 +334,18 @@ def add_vertices(self, n): else: names = n result = GraphBase.add_vertices(self, len(names)) - self.vs[m:]["name"] = names - return result - return GraphBase.add_vertices(self, n) + if len(names) > 0: + self.vs[m:]["name"] = names + if attributes is not None: + for key, val in attributes.items(): + self.vs[m:][key] = val + else: + result = GraphBase.add_vertices(self, n) + if (attributes is not None) and (n > 0): + m = self.vcount() - n + for key, val in attributes.items(): + self.vs[m:][key] = val + return result def adjacent(self, *args, **kwds): """adjacent(vertex, mode=OUT) diff --git a/tests/test_basic.py b/tests/test_basic.py index 88c7deea4..6deebd4d7 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -120,20 +120,26 @@ def testAddVertex(self): self.assertEqual( sorted(g.vertex_attributes()), ["ham", "name", "spam"] ) - self.assertEqual(g.vs["spam"], [None]*4 + ["cheese"]) - self.assertEqual(g.vs["ham"], [None]*4 + [42]) + self.assertEqual(g.vs["spam"], [None] * 4 + ["cheese"]) + self.assertEqual(g.vs["ham"], [None] * 4 + [42]) def testAddVertices(self): g = Graph() g.add_vertices(2) self.assertTrue(g.vcount() == 2 and g.ecount() == 0) + g.add_vertices("spam") self.assertTrue(g.vcount() == 3 and g.ecount() == 0) self.assertEqual(g.vs[2]["name"], "spam") + g.add_vertices(["bacon", "eggs"]) self.assertTrue(g.vcount() == 5 and g.ecount() == 0) self.assertEqual(g.vs[2:]["name"], ["spam", "bacon", "eggs"]) + g.add_vertices(2, attributes={'color': ['k', 'b']}) + self.assertEqual(g.vs[2:]["name"], ["spam", "bacon", "eggs", None, None]) + self.assertEqual(g.vs[5:]["color"], ["k", "b"]) + def testDeleteVertices(self): g = Graph([(0, 1), (1, 2), (2, 3), (0, 2), (3, 4), (4, 5)]) self.assertEqual(6, g.vcount()) @@ -208,6 +214,14 @@ def testAddEdges(self): (0, 1), (1, 2), (2, 3), (1, 3), (0, 2), (0, 3) ]) + g.add_edges([(0, 0), (1, 1)], attributes={'color': ['k', 'b']}) + self.assertEqual(g.get_edgelist(), [ + (0, 1), (1, 2), (2, 3), (1, 3), (0, 2), (0, 3), (0, 0), (1, 1), + ]) + self.assertEqual( + g.es['color'], + [None, None, None, None, None, None, 'k', 'b']) + def testDeleteEdges(self): g = Graph.Famous("petersen") g.vs["name"] = list("ABCDEFGHIJ") From 319aa82ceff0f62ebdb1eeac7355e842e8dbdf7f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 31 Jul 2020 14:15:21 +0200 Subject: [PATCH 0025/1681] add Python 3.8 to tox virtualenvs --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4806db4af..e47224111 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, pypy, pypy3 +envlist = py27, py35, py36, py37, py38, pypy, pypy3 [testenv] commands = python -m unittest From 52678603271fcb3151abc57fca388c585689b596 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 4 Aug 2020 04:14:30 +1000 Subject: [PATCH 0026/1681] DFS and DFS iterator (#315) --- src/_igraph/dfsiter.c | 295 +++++++++++++++++++++++++++++++++++++ src/_igraph/dfsiter.h | 53 +++++++ src/_igraph/graphobject.c | 29 ++++ src/_igraph/igraphmodule.c | 4 + src/igraph/__init__.py | 44 ++++++ tests/test_iterators.py | 42 +++++- 6 files changed, 459 insertions(+), 8 deletions(-) create mode 100644 src/_igraph/dfsiter.c create mode 100644 src/_igraph/dfsiter.h diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c new file mode 100644 index 000000000..d7d8105af --- /dev/null +++ b/src/_igraph/dfsiter.c @@ -0,0 +1,295 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2020 The igraph development team + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + +*/ + +#include "dfsiter.h" +#include "common.h" +#include "error.h" +#include "py2compat.h" +#include "vertexobject.h" + +/** + * \ingroup python_interface + * \defgroup python_interface_dfsiter DFS iterator object + */ + +PyTypeObject igraphmodule_DFSIterType; + +/** + * \ingroup python_interface_dfsiter + * \brief Allocate a new DFS iterator object for a given graph and a given root + * \param g the graph object being referenced + * \param vid the root vertex index + * \param advanced whether the iterator should be advanced (returning distance and parent as well) + * \return the allocated PyObject + */ +PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { + igraphmodule_DFSIterObject* o; + long int no_of_nodes, r; + + o=PyObject_GC_New(igraphmodule_DFSIterObject, &igraphmodule_DFSIterType); + Py_INCREF(g); + o->gref=g; + o->graph=&g->g; + + if (!PyInt_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { + PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); + return NULL; + } + + no_of_nodes=igraph_vcount(&g->g); + o->visited=(char*)calloc(no_of_nodes, sizeof(char)); + if (o->visited == 0) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + return NULL; + } + + if (igraph_stack_init(&o->stack, 100)) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + return NULL; + } + if (igraph_vector_init(&o->neis, 0)) { + PyErr_SetString(PyExc_MemoryError, "out of memory"); + igraph_stack_destroy(&o->stack); + return NULL; + } + + if (PyInt_Check(root)) { + r=PyInt_AsLong(root); + } else { + r = ((igraphmodule_VertexObject*)root)->idx; + } + /* push the root onto the stack */ + if (igraph_stack_push(&o->stack, r) || + igraph_stack_push(&o->stack, 0) || + igraph_stack_push(&o->stack, -1)) { + igraph_stack_destroy(&o->stack); + igraph_vector_destroy(&o->neis); + PyErr_SetString(PyExc_MemoryError, "out of memory"); + return NULL; + } + o->visited[r] = 1; + + if (!igraph_is_directed(&g->g)) mode=IGRAPH_ALL; + o->mode=mode; + o->advanced=advanced; + + PyObject_GC_Track(o); + + RC_ALLOC("DFSIter", o); + + return (PyObject*)o; +} + +/** + * \ingroup python_interface_dfsiter + * \brief Support for cyclic garbage collection in Python + * + * This is necessary because the \c igraph.DFSIter object contains several + * other \c PyObject pointers and they might point back to itself. + */ +int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, + visitproc visit, void *arg) { + int vret; + + RC_TRAVERSE("DFSIter", self); + + if (self->gref) { + vret=visit((PyObject*)self->gref, arg); + if (vret != 0) return vret; + } + + return 0; +} + +/** + * \ingroup python_interface_dfsiter + * \brief Clears the iterator's subobject (before deallocation) + */ +int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { + PyObject *tmp; + + PyObject_GC_UnTrack(self); + + tmp=(PyObject*)self->gref; + self->gref = NULL; + Py_XDECREF(tmp); + + igraph_stack_destroy(&self->stack); + igraph_vector_destroy(&self->neis); + free(self->visited); + self->visited = 0; + + return 0; +} + +/** + * \ingroup python_interface_dfsiter + * \brief Deallocates a Python representation of a given DFS iterator object + */ +void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { + igraphmodule_DFSIter_clear(self); + + RC_DEALLOC("DFSIter", self); + + PyObject_GC_Del(self); +} + +PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { + Py_INCREF(self); + return (PyObject*)self; +} + +PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { + /* the design is to return the top of the stack and then proceed until + * we have found an unvisited neighbor and push that on top */ + igraph_integer_t parent_out, dist_out, vid_out; + igraph_bool_t any = 0; + + /* nothing on the stack, end of iterator */ + if(igraph_stack_empty(&self->stack)) { + return NULL; + } + + /* peek at the top element on the stack + * because we save three things, pop 3 in inverse order and push them back */ + parent_out = (igraph_integer_t)igraph_stack_pop(&self->stack); + dist_out = (igraph_integer_t)igraph_stack_pop(&self->stack); + vid_out = (igraph_integer_t)igraph_stack_pop(&self->stack); + igraph_stack_push(&self->stack, (long int) vid_out); + igraph_stack_push(&self->stack, (long int) dist_out); + igraph_stack_push(&self->stack, (long int) parent_out); + + /* look for neighbors until you found one or until you have exausted the graph */ + while (!any && !igraph_stack_empty(&self->stack)) { + igraph_integer_t parent = (igraph_integer_t)igraph_stack_pop(&self->stack); + igraph_integer_t dist = (igraph_integer_t)igraph_stack_pop(&self->stack); + igraph_integer_t vid = (igraph_integer_t)igraph_stack_pop(&self->stack); + igraph_stack_push(&self->stack, (long int) vid); + igraph_stack_push(&self->stack, (long int) dist); + igraph_stack_push(&self->stack, (long int) parent); + long int i; + /* the values above are returned at at this stage. However, we must + * prepare for the next iteration by putting the next unvisited + * neighbor onto the stack */ + if (igraph_neighbors(self->graph, &self->neis, vid, self->mode)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + for (i=0; ineis); i++) { + igraph_integer_t neighbor = (igraph_integer_t)VECTOR(self->neis)[i]; + /* new neighbor, push the next item onto the stack */ + if (self->visited[neighbor] == 0) { + any = 1; + self->visited[neighbor]=1; + if (igraph_stack_push(&self->stack, neighbor) || + igraph_stack_push(&self->stack, dist+1) || + igraph_stack_push(&self->stack, vid)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + break; + } + } + /* no new neighbors, end of subtree */ + if (!any) { + igraph_stack_pop(&self->stack); + igraph_stack_pop(&self->stack); + igraph_stack_pop(&self->stack); + } + } + + /* no matter what the stack situation is: that is a worry for the next cycle + * now just return the top of the stack as it was at the function entry */ + PyObject *vertexobj = igraphmodule_Vertex_New(self->gref, vid_out); + if (self->advanced) { + PyObject *parentobj; + if (!vertexobj) + return NULL; + if (parent_out >= 0) { + parentobj = igraphmodule_Vertex_New(self->gref, parent_out); + if (!parentobj) + return NULL; + } else { + Py_INCREF(Py_None); + parentobj=Py_None; + } + return Py_BuildValue("NlN", vertexobj, (long int)dist_out, parentobj); + } else { + return vertexobj; + } +} + +/** + * \ingroup python_interface_dfsiter + * Method table for the \c igraph.DFSIter object + */ +PyMethodDef igraphmodule_DFSIter_methods[] = { + {NULL} +}; + +/** \ingroup python_interface_dfsiter + * Python type object referencing the methods Python calls when it performs various operations on + * a DFS iterator of a graph + */ +PyTypeObject igraphmodule_DFSIterType = +{ + PyVarObject_HEAD_INIT(0, 0) + "igraph.DFSIter", // tp_name + sizeof(igraphmodule_DFSIterObject), // tp_basicsize + 0, // tp_itemsize + (destructor)igraphmodule_DFSIter_dealloc, // tp_dealloc + 0, // tp_print + 0, // tp_getattr + 0, // tp_setattr + 0, /* tp_compare (2.x) / tp_reserved (3.x) */ + 0, // tp_repr + 0, // tp_as_number + 0, // tp_as_sequence + 0, // tp_as_mapping + 0, // tp_hash + 0, // tp_call + 0, // tp_str + 0, // tp_getattro + 0, // tp_setattro + 0, // tp_as_buffer + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, // tp_flags + "igraph DFS iterator object", // tp_doc + (traverseproc) igraphmodule_DFSIter_traverse, /* tp_traverse */ + (inquiry) igraphmodule_DFSIter_clear, /* tp_clear */ + 0, // tp_richcompare + 0, // tp_weaklistoffset + (getiterfunc)igraphmodule_DFSIter_iter, /* tp_iter */ + (iternextfunc)igraphmodule_DFSIter_iternext, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ + 0, /* tp_free */ +}; + diff --git a/src/_igraph/dfsiter.h b/src/_igraph/dfsiter.h new file mode 100644 index 000000000..0a094c592 --- /dev/null +++ b/src/_igraph/dfsiter.h @@ -0,0 +1,53 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2020 The igraph development team + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + +*/ + +#ifndef PYTHON_DFSITER_H +#define PYTHON_DFSITER_H + +#include +#include "graphobject.h" + +/** + * \ingroup python_interface_dfsiter + * \brief A structure representing a DFS iterator of a graph + */ +typedef struct +{ + PyObject_HEAD + igraphmodule_GraphObject* gref; + igraph_stack_t stack; + igraph_vector_t neis; + igraph_t *graph; + char *visited; + igraph_neimode_t mode; + igraph_bool_t advanced; +} igraphmodule_DFSIterObject; + +PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *o, igraph_neimode_t mode, igraph_bool_t advanced); +int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, + visitproc visit, void *arg); +int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self); +void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self); + +extern PyTypeObject igraphmodule_DFSIterType; + +#endif diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 89ae5ba56..bb1789bd7 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -23,6 +23,7 @@ #include "attributes.h" #include "arpackobject.h" #include "bfsiter.h" +#include "dfsiter.h" #include "common.h" #include "convert.h" #include "edgeseqobject.h" @@ -9920,6 +9921,23 @@ PyObject *igraphmodule_Graph_unfold_tree(igraphmodule_GraphObject * self, return Py_BuildValue("NN", result_o, mapping_o); } +/** \ingroup python_interface_graph + * \brief Constructs a depth first search (DFS) iterator of the graph + */ +PyObject *igraphmodule_Graph_dfsiter(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + char *kwlist[] = { "vid", "mode", "advanced", NULL }; + PyObject *root, *adv = Py_False, *mode_o = Py_None; + igraph_neimode_t mode = IGRAPH_OUT; + + if (!PyArg_ParseTupleAndKeywords + (args, kwds, "O|OO", kwlist, &root, &mode_o, &adv)) + return NULL; + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; + return igraphmodule_DFSIter_new(self, root, mode, PyObject_IsTrue(adv)); +} + /********************************************************************** * Dominator * **********************************************************************/ @@ -14517,6 +14535,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " returns the distance of the vertex from the root and the\n" " parent of the vertex in the BFS tree as well.\n" "@return: the BFS iterator as an L{igraph.BFSIter} object.\n"}, + {"dfsiter", (PyCFunction) igraphmodule_Graph_dfsiter, + METH_VARARGS | METH_KEYWORDS, + "dfsiter(vid, mode=OUT, advanced=False)\n\n" + "Constructs a depth first search (DFS) iterator of the graph.\n\n" + "@param vid: the root vertex ID\n" + "@param mode: either L{IN} or L{OUT} or L{ALL}.\n" + "@param advanced: if C{False}, the iterator returns the next\n" + " vertex in DFS order in every step. If C{True}, the iterator\n" + " returns the distance of the vertex from the root and the\n" + " parent of the vertex in the DFS tree as well.\n" + "@return: the DFS iterator as an L{igraph.DFSIter} object.\n"}, ///////////////// // CONVERSIONS // diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 05a0d16ec..fcd79063e 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -26,6 +26,7 @@ #include "arpackobject.h" #include "attributes.h" #include "bfsiter.h" +#include "dfsiter.h" #include "common.h" #include "convert.h" #include "edgeobject.h" @@ -764,6 +765,8 @@ extern PyObject* igraphmodule_arpack_options_default; INITERROR; if (PyType_Ready(&igraphmodule_BFSIterType) < 0) INITERROR; + if (PyType_Ready(&igraphmodule_DFSIterType) < 0) + INITERROR; if (PyType_Ready(&igraphmodule_ARPACKOptionsType) < 0) INITERROR; @@ -783,6 +786,7 @@ extern PyObject* igraphmodule_arpack_options_default; /* Add the types to the core module */ PyModule_AddObject(m, "GraphBase", (PyObject*)&igraphmodule_GraphType); PyModule_AddObject(m, "BFSIter", (PyObject*)&igraphmodule_BFSIterType); + PyModule_AddObject(m, "DFSIter", (PyObject*)&igraphmodule_DFSIterType); PyModule_AddObject(m, "ARPACKOptions", (PyObject*)&igraphmodule_ARPACKOptionsType); PyModule_AddObject(m, "Edge", (PyObject*)&igraphmodule_EdgeType); PyModule_AddObject(m, "EdgeSeq", (PyObject*)&igraphmodule_EdgeSeqType); diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 5f999b6a8..96814300f 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3240,6 +3240,50 @@ def get_incidence(self, types="type", *args, **kwds): """ return super(Graph, self).get_incidence(types, *args, **kwds) + ########################### + # DFS (C version will come soon) + def dfs(self, vid, mode=OUT): + """Conducts a depth first search (DFS) on the graph. + + @param vid: the root vertex ID + @param mode: either L{IN} or L{OUT} or L{ALL}, ignored + for undirected graphs. + @return: a tuple with the following items: + - The vertex IDs visited (in order) + - The parent of every vertex in the DFS + """ + nv = self.vcount() + added = [False for v in range(nv)] + stack = [] + + # prepare output + vids = [] + parents = [] + + # ok start from vid + stack.append(vid) + vids.append(vid) + parents.append(vid) + added[vid] = True + + # go down the rabbit hole + while stack: + vid = stack[-1] + neighbors = self.neighbors(vid, mode=mode) + for neighbor in neighbors: + if not added[neighbor]: + # Add hanging subtree neighbor + stack.append(neighbor) + vids.append(neighbor) + parents.append(vid) + added[neighbor] = True + break + else: + # No neighbor found, end of subtree + stack.pop() + + return (vids, parents) + ########################### # ctypes support diff --git a/tests/test_iterators.py b/tests/test_iterators.py index a229e4383..7bd6d81bf 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -3,13 +3,40 @@ class IteratorTests(unittest.TestCase): def testBFS(self): - g=Graph.Tree(10, 2) - vs=[v.index for v in g.bfsiter(0)] + g = Graph.Tree(10, 2) + vs, layers, ps = g.bfs(0) self.assertEqual(vs, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - vs=[(v.index,dist,parent) for v,dist,parent in g.bfsiter(0, advanced=True)] - vs=[(v,d,p.index) for v,d,p in vs if p != None] - self.assertEqual(vs, [(1,1,0), (2,1,0), (3,2,1), (4,2,1), \ - (5,2,2), (6,2,2), (7,3,3), (8,3,3), (9,3,4)]) + self.assertEqual(ps, [0, 0, 0, 1, 1, 2, 2, 3, 3, 4]) + + def testBFSIter(self): + g = Graph.Tree(10, 2) + vs = [v.index for v in g.bfsiter(0)] + self.assertEqual(vs, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + vs = [(v.index, d, p) for v, d, p in g.bfsiter(0, advanced=True)] + vs = [(v, d, p.index) for v, d, p in vs if p is not None] + self.assertEqual( + vs, + [(1, 1, 0), (2, 1, 0), (3, 2, 1), + (4, 2, 1), (5, 2, 2), (6, 2, 2), + (7, 3, 3), (8, 3, 3), (9, 3, 4)]) + + def testDFS(self): + g = Graph.Tree(10, 2) + vs, ps = g.dfs(0) + self.assertEqual(vs, [0, 1, 3, 7, 8, 4, 9, 2, 5, 6]) + self.assertEqual(ps, [0, 0, 1, 3, 3, 1, 4, 0, 2, 2]) + + def testDFSIter(self): + g = Graph.Tree(10, 2) + vs = [v.index for v in g.dfsiter(0)] + self.assertEqual(vs, [0, 1, 3, 7, 8, 4, 9, 2, 5, 6]) + vs = [(v.index, d, p) for v, d, p in g.dfsiter(0, advanced=True)] + vs = [(v, d, p.index) for v, d, p in vs if p is not None] + self.assertEqual( + vs, + [(1, 1, 0), (3, 2, 1), (7, 3, 3), + (8, 3, 3), (4, 2, 1), (9, 3, 4), + (2, 1, 0), (5, 2, 2), (6, 2, 2)]) def suite(): @@ -19,7 +46,6 @@ def suite(): def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + if __name__ == "__main__": test() - From 5f7530a944a9354d39a61e091b26104d9af603c7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 4 Aug 2020 04:21:17 +1000 Subject: [PATCH 0027/1681] Docs on graph analysis (#314) --- doc/source/analysis.rst | 427 +++++++++++++++++++++++++++++++++++++- doc/source/generation.rst | 4 + src/_igraph/graphobject.c | 2 +- 3 files changed, 431 insertions(+), 2 deletions(-) diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 8b0ff457f..a4ab5ec74 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -1,5 +1,430 @@ Graph analysis ============== +|igraph| enables analysis of graphs/networks from simple operations such as adding and removing nodes to complex theoretical constructs such as community detection. Read the `API documentation`_ for details on each function and class. -.. note:: TODO. This is a placeholder section; it is not written yet. +The context for the following examples will be to import |igraph| (commonly as `ig`), have the :class:`Graph` class and to have one or more graphs available: +>>> import igraph as ig +>>> from igraph import Graph +>>> g = Graph(edges=[[0, 1], [2, 3]]) + +To get a summary representation of the graph, use :meth:`Graph.summary`. For instance + +>>> g.summary(verbosity=1) + +will provide a fairly detailed description. + +To copy a graph, use :meth:`Graph.copy`. This is a "shallow" copy: any mutable objects in the attributes are not copied (they would refer to the same instance). +If you want to copy a graph including all its attributes, use Python's `deepcopy` module. + +Vertices and edges ++++++++++++++++++++++++++++ +Vertices are numbered 0 to `n-1`, where n is the number of vertices in the graph. These are called the "vertex ids". +To count vertices, use :meth:`Graph.vcount`: + +>>> n = g.vcount() + +Edges also have ids from 0 to `m-1` and are counted by :meth:`Graph.ecount`: + +>>> m = g.ecount() + +To get a sequence of vertices, use their ids and :attr:`Graph.vs`: + +>>> for v in g.vs: +>>> print(v) + +Similarly for edges, use :attr:`Graph.es`: + +>>> for e in g.es: +>>> print(e) + +You can index and slice vertices and edges similar to a list: + +>>> g.vs[:4] +>>> g.vs[0, 2, 4] +>>> g.es[3] + +.. note:: The `vs` and `es` attributes are special sequences with their own useful methods. See `API documentation`_ for a full list. + +If you prefer a vanilla edge list, you can use :meth:`Graph.get_edge_list`. + +Incidence +++++++++++++++++++++++++++++++ +To get the vertices at the two ends of an edge, use :attr:`Edge.source` and :attr:`Edge.target`: + +>>> e = g.es[0] +>>> v1, v2 = e.source, e.target + +Vice versa, to get the edge if from the source and target vertices, you can use :meth:`Graph.get_eid` or, for multiple pairs of source/targets, +:meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_connected`. + +To get the edges incident on a vertex, you can use :meth:`Vertex.incident`, :meth:`Vertex.out_edges` and +:meth:`Vertex.in_edges`. The three are equivalent on undirected graphs but not directed ones, of course: + +>>> v = g.vs[0] +>>> edges = v.incident() + +The :meth:`Graph.incident` function fulfills the same purpose with a slightly different syntax based on vertex ids: + +>>> edges = g.incident(0) + +To get the full adjacency/incidence list representation of the graph, use :meth:`Graph.get_adjlist`, :meth:`Graph.g.get_inclist()` or, for a bipartite graph, :meth:`Graph.get_incidence`. + +Neighborhood ++++++++++++++ +To compute the neighbors, successors, and predecessors, the methods :meth:`Graph.neighbors`, :meth:`Graph.successors` and +:meth:`Graph.predecessors` are available. The three give the same answer in undirected graphs and have a similar dual syntax: + +>>> neis = g.vs[0].neighbors() +>>> neis = g.neighbors(0) + +To get the list of vertices within a certain distance from one or more initial vertices, you can use :meth:`Graph.neighborhood`: + +>>> g.neighborhood([0, 1], order=2) + +and to find the neighborhood size, there is :meth:`Graph.neighborhood_size`. + +Degrees ++++++++ +To compute the degree, in-degree, or out-degree of a node, use :meth:`Vertex.degree`, :meth:`Vertex.indegree`, and :meth:`Vertex.outdegree`: + +>>> deg = g.vs[0].degree() +>>> deg = g.degree(0) + +To compute the max degree in a list of vertices, use :meth:`Graph.maxdegree`. + +:meth:`Graph.knn` computes the average degree of the neighbors. + +Adding and removing vertices and edges +++++++++++++++++++++++++++++++++++++++ + +To add nodes to a graph, use :meth:`Graph.add_vertex` and :meth:`Graph.add_vertices`: + +>>> g.add_vertex() +>>> g.add_vertices(5) + +This changes the graph `g` in place. You can specify the name of the vertices if you wish. + +To remove nodes, use :meth:`Graph.delete_vertices`: + +>>> g.delete_vertices([1, 2]) + +Again, you can specify the names or the actual :class:`Vertex` objects instead. + +To add edges, use :meth:`Graph.add_edge` and :meth:`Graph.add_edges`: + +>>> g.add_edge(0, 2) +>>> g.add_edges([(0, 2), (1, 3)]) + +To remove edges, use :meth:`Graph.delete_edges`: + +>>> g.delete_edges([0, 5]) # remove by edge id + +You can also remove edges between source and target nodes. + +To contract vertices, use :meth:`Graph.contract_vertices`. Edges between contracted vertices will become loops. + +Graph operators ++++++++++++++++++ +It is possible to compute the union, intersection, difference, and other set operations (operators) between graphs. + +To compute the union of the graphs (nodes/edges in either are kept): + +>>> gu = ig.union([g, g2, g3]) + +Similarly for the intersection (nodes/edges present in all are kept): + +>>> gu = ig.intersection([g, g2, g3]) + +These two operations preserve attributes and can be performed with a few variations. The most important one is that vertices can be matched across the graphs by id (number) or by name. + +These and other operations are also available as methods of the :class:`Graph` class: + +>>> g.union(g2) +>>> g.intersection(g2) +>>> g.disjoint_union(g2) +>>> g.difference(g2) +>>> g.complementer() # complement graph, same nodes but missing edges + +and even as numerical operators: + +>>> g |= g2 +>>> g_intersection = g and g2 + +Topological sorting ++++++++++++++++++++ +To sort a graph topologically, use :meth:`Graph.topological_sorting`: + +>>> g = ig.Graph.Tree(10, 2, mode=ig.TREE_OUT) +>>> g.topological_sorting() + +Graph traversal ++++++++++++++++++++++ +A common operation is traversing the graph. |igraph| currently exposes breadth-first search (BFS) via :meth:`Graph.bfs` and :meth:`Graph.bfsiter`: + +>>> [vertices, layers, parents] = g.bfs() +>>> it = g.bfsiter() # Lazy version + +Depth-first search has a similar infrastructure via :meth:`Graph.dfs` and :meth:`Graph.dfsiter`: + +>>> [vertices, parents] = g.dfs() +>>> it = g.dfsiter() # Lazy version + +To perform a random walk from a certain vertex, use :meth:`Graph.random_walk`: + +>>> vids = g.random_walk(0, 3) + +Pathfinding and cuts +++++++++++++++++++++ +Several pathfinding algorithms are available: + +- :meth:`Graph.shortest_paths` or :meth:`Graph.get_shortest_paths` +- :meth:`Graph.get_all_shortest_paths` +- :meth:`Graph.get_all_simple_paths` +- :meth:`Graph.spanning_tree` finds a minimum spanning tree + +As well as functions related to cuts and paths: + +- :meth:`Graph.mincut` calculates the minimum cut between the source and target vertices +- :meth:`Graph.st_mincut` - as previous one, but returns a simpler data structure +- :meth:`Graph.mincut_value` - as previous one, but returns only the value +- :meth:`Graph.all_st_cuts` +- :meth:`Graph.all_st_mincuts` +- :meth:`Graph.edge_connectivity` or :meth:`Graph.edge_disjoint_paths` or :meth:`Graph.adhesion` +- :meth:`Graph.vertex_connectivity` or :meth:`Graph.cohesion` + +See also the section on flow. + +Global properties ++++++++++++++++++++++ +A number of global graph measures are available. + +Basic: + +- :meth:`Graph.diameter` or :meth:`Graph.get_diameter` +- :meth:`Graph.girth` +- :meth:`Graph.radius` +- :meth:`Graph.average_path_length` + +Distributions: + +- :meth:`Graph.degree_distribution` +- :meth:`Graph.path_length_hist` + +Connectedness: + +- :meth:`Graph.all_minimal_st_separators` +- :meth:`Graph.minimum_size_separators` +- :meth:`Graph.cut_vertices` or :meth:`Graph.articulation_points` + +Cliques and motifs: + +- :meth:`Graph.clique_number` (aka :meth:`Graph.omega`) +- :meth:`Graph.cliques` +- :meth:`Graph.maximal_cliques` +- :meth:`Graph.largest_cliques` +- :meth:`Graph.motifs_randesu` and :meth:`Graph.motifs_randesu_estimate` +- :meth:`Graph.motifs_randesu_no` counts the number of motifs + +Directed acyclic graphs: + +- :meth:`Graph.is_dag` +- :meth:`Graph.feedback_arc_set` +- :meth:`Graph.topological_sorting` + +Optimality: + +- :meth:`Graph.farthest_points` +- :meth:`Graph.modularity` +- :meth:`Graph.maximal_cliques` +- :meth:`Graph.largest_cliques` +- :meth:`Graph.independence_number` (aka :meth:`Graph.alpha`) +- :meth:`Graph.maximal_independent_vertex_sets` +- :meth:`Graph.largest_independent_vertex_sets` +- :meth:`Graph.mincut` +- :meth:`Graph.mincut_value` +- :meth:`Graph.feedback_arc_set` +- :meth:`Graph.maximum_bipartite_matching` (bipartite graphs) + +Other complex measures are: + +- :meth:`Graph.assortativity` +- :meth:`Graph.assortativity_degree` +- :meth:`Graph.assortativity_nominal` +- :meth:`Graph.density` +- :meth:`Graph.transitivity_undirected` +- :meth:`Graph.transitivity_avglocal_undirected` +- :meth:`Graph.dyad_census` +- :meth:`Graph.triad_census` +- :meth:`Graph.reciprocity` (directed graphs) +- :meth:`Graph.isoclass` (only 3 or 4 vertices) +- :meth:`Graph.biconnected_components` aka :meth:`Graph.blocks` + +Boolean properties: + +- :meth:`Graph.is_bipartite` +- :meth:`Graph.is_connected` +- :meth:`Graph.is_dag` +- :meth:`Graph.is_directed` +- :meth:`Graph.is_named` +- :meth:`Graph.is_simple` +- :meth:`Graph.is_weighted` +- :meth:`Graph.has_multiple` + +Vertex properties ++++++++++++++++++++ +A spectrum of vertex-level properties can be computed. Similarity measures include: + +- :meth:`Graph.similarity_dice` +- :meth:`Graph.similarity_jaccard` +- :meth:`Graph.similarity_inverse_log_weighted` +- :meth:`Graph.diversity` + +Structural: + +- :meth:`Graph.authority_score` +- :meth:`Graph.hub_score` +- :meth:`Graph.betweenness` +- :meth:`Graph.bibcoupling` +- :meth:`Graph.closeness` +- :meth:`Graph.constraint` +- :meth:`Graph.cocitation` +- :meth:`Graph.coreness` (aka :meth:`Graph.shell_index`) +- :meth:`Graph.eccentricity` +- :meth:`Graph.eigenvector_centrality` +- :meth:`Graph.pagerank` +- :meth:`Graph.personalized_pagerank` +- :meth:`Graph.strength` +- :meth:`Graph.transitivity_local_undirected` + +Connectedness: + +- :meth:`Graph.subcomponent` +- :meth:`Graph.is_separator` +- :meth:`Graph.is_minimal_separator` + +Edge properties ++++++++++++++++ +As for vertices, edge properties are implemented. Basic properties include: + +- :meth:`Graph.is_loop` +- :meth:`Graph.is_multiple` +- :meth:`Graph.is_mutual` +- :meth:`Graph.count_multiple` + +and more complex ones: + +- :meth:`Graph.edge_betweenness` + +Matrix representations ++++++++++++++++++++++++ +Matrix-related functionality includes: + +- :meth:`Graph.get_adjacency` +- :meth:`Graph.get_adjacency_sparse` (sparse CSR matrix version) +- :meth:`Graph.laplacian` + +Clustering +++++++++++ +|igraph| includes several approaches to unsupervised graph clustering and community detection: + +- :meth:`Graph.components` (aka :meth:`Graph.clusters`): the connected components +- :meth:`Graph.cohesive_blocks` +- :meth:`Graph.community_edge_betweenness` +- :meth:`Graph.community_fastgreedy` +- :meth:`Graph.community_infomap` +- :meth:`Graph.community_label_propagation` +- :meth:`Graph.community_leading_eigenvector` +- :meth:`Graph.community_leading_eigenvector_naive` +- :meth:`Graph.community_leiden` +- :meth:`Graph.community_multilevel` (a version of Louvain) +- :meth:`Graph.community_optimal_modularity` (exact solution, < 100 vertices) +- :meth:`Graph.community_spinglass` +- :meth:`Graph.community_walktrap` + +Simplification, permutations and rewiring ++++++++++++++++++++++++++++++++++++++++++ +To check is a graph is simple, you can use :meth:`Graph.is_simple`. + +>>> g.is_simple() + +To simplify a graph (remove multiedges and loops), use :meth:`Graph.simplify`: + +>>> g_simple = g.simplify() + +To return a directed/undirected copy of the graph, use :meth:`Graph.as_directed` and :meth:`Graph.as_undirected`, respectively. + +To permute the order of vertices, you can use :meth:`Graph.permute_vertices`: + +>>> g = ig.Tree(6, 2) +>>> g_perm = g.permute_vertices([1, 0, 2, 3, 4, 5]) + +The canonical permutation can be obtained via :meth:`Graph.canonical_permutation`, which can then be directly passed to the function above. + +To rewire the graph at random, there are: + +- :meth:`Graph.rewire` - preserves the degree distribution +- :meth:`Graph.rewire_edges` - fixed rewiring probability for each endpoint + +Line graph +++++++++++ +To compute the line graph of a graph `g`, which represents the connectedness of the *edges* of g, you can use :meth:`Graph.linegraph`: + +>>> g = Graph(n=4, edges=[[0, 1], [0, 2]]) +>>> gl = g.linegraph() + +In this case, the line graph has two vertices, representing the two edges of the original graph, and one edge, representing the point where +those two original edges touch. + +Composition and subgraphs +++++++++++++++++++++++++++ +The function :meth:`Graph.decompose` decomposes the graph into subgraphs. Vice versa, the function :meth:`Graph.compose` returns the composition of two graphs. + +To compute the subgraph spannes by some vertices/edges, use :meth:`Graph.subgraph` (aka :meth:`Graph.induced_subgraph`) and :meth:`Graph.subgraph_edges`: + +>>> g_sub = g.subgraph([0, 1]) +>>> g_sub = g.subgraph_edges([0]) + +To compute the minimum spanning tree, use :meth:`Graph.spanning_tree`. + +To compute graph k-cores, the method :meth:`Graph.k_core` is available. + +The dominator tree from a given node can be obtained with :meth:`Graph.dominator`. + +Bipartite graphs can be decomposed using :meth:`Graph.bipartite_projection`. The size of the projections can be computed using :meth:`Graph.bipartite_projection_size`. + +Morphisms +++++++++++++++++++ +|igraph| enables comparisons between graphs: + +- :meth:`Graph.isomorphic` +- :meth:`Graph.isomorphic_vf2` +- :meth:`Graph.subisomorphic_vf2` +- :meth:`Graph.subisomorphic_lad` +- :meth:`Graph.get_isomorphisms_vf2` +- :meth:`Graph.get_subisomorphisms_vf2` +- :meth:`Graph.get_subisomorphisms_lad` +- :meth:`Graph.get_automorphisms_vf2` +- :meth:`Graph.count_isomorphisms_vf2` +- :meth:`Graph.count_subisomorphisms_vf2` +- :meth:`Graph.count_automorphisms_vf2` + +Flow +++++ +Flow is a characteristic of directed graphs. The following functions are available: + +- :meth:`Graph.maxflow` between two nodes +- :meth:`Graph.maxflow_value` - similar to the previous one, but only the value is returned +- :meth:`Graph.gomory_hu_tree` + +Flow and cuts are closely related, therefore you might find the following functions useful as well: + +- :meth:`Graph.mincut` calculates the minimum cut between the source and target vertices +- :meth:`Graph.st_mincut` - as previous one, but returns a simpler data structure +- :meth:`Graph.mincut_value` - as previous one, but returns only the value +- :meth:`Graph.all_st_cuts` +- :meth:`Graph.all_st_mincuts` +- :meth:`Graph.edge_connectivity` or :meth:`Graph.edge_disjoint_paths` or :meth:`Graph.adhesion` +- :meth:`Graph.vertex_connectivity` or :meth:`Graph.cohesion` + +.. _API documentation: https://igraph.org/python/doc/igraph-module.html diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 3c42c39e4..a3f5b952e 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -7,6 +7,10 @@ The :class:`Graph` class is the main object used to generate graphs: >>> from igraph import Graph +To copy a graph, use :meth:`Graph.copy`: + +>>> g_new = g.copy() + From nodes and edges ++++++++++++++++++++ Nodes are always numbered from 0 upwards. To create a generic graph with a specified number of nodes (e.g. 10) and a list of edges between them, you can use the generic constructor: diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bb1789bd7..3709fc569 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13096,7 +13096,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " the algorithm assumes that there might be some loops in the graph\n" " and calculates the density accordingly. If C{False}, the algorithm\n" " assumes that there can't be any loops.\n" - "@return: the reciprocity of the graph."}, + "@return: the density of the graph."}, /* interfaces to igraph_diameter */ {"diameter", (PyCFunction) igraphmodule_Graph_diameter, From 2d28d2f72f4efc6f5baef61ab599a520c5fbadc0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 4 Aug 2020 13:45:55 +0200 Subject: [PATCH 0028/1681] trying to fix AppVeyor --- appveyor.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a602baa12..7e3ea6a77 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,10 +23,16 @@ platform: - x64 install: + # update msys2 keyring first + - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" + - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig" + - bash -lc "pacman --noconfirm -U msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" + + # update msys2 - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors" - bash -lc "pacman --noconfirm -Sy" - - bash -lc "pacman --noconfirm -S zstd" - - bash -lc "pacman --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" + - bash -lc "pacman --needed --noconfirm -S zstd" + - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - pip install cibuildwheel==1.1.0 before_build: From 2fa27a958f8509ce686a34e62aca6a2ca7dba011 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 4 Aug 2020 13:46:50 +0200 Subject: [PATCH 0029/1681] ignore VSCode files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2a22ad337..06705accc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ igraph/*.so .eggs/ .tox .venv/ +.vscode/ docker/wheelhouse vendor/build/ vendor/install/ From 5e4a5e2bc10b4ae9d7a9ede6343f55d7e8cdb82a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 4 Aug 2020 13:47:24 +0200 Subject: [PATCH 0030/1681] DFS: query neighbors only once for each vertex, don't query when backtracking --- src/igraph/__init__.py | 12 ++++++------ tests/test_iterators.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 96814300f..248754144 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3261,23 +3261,23 @@ def dfs(self, vid, mode=OUT): parents = [] # ok start from vid - stack.append(vid) + stack.append((vid, self.neighbors(vid, mode=mode))) vids.append(vid) parents.append(vid) added[vid] = True # go down the rabbit hole while stack: - vid = stack[-1] - neighbors = self.neighbors(vid, mode=mode) - for neighbor in neighbors: + vid, neighbors = stack[-1] + if neighbors: + # Get next neighbor to visit + neighbor = neighbors.pop() if not added[neighbor]: # Add hanging subtree neighbor - stack.append(neighbor) + stack.append((neighbor, self.neighbors(neighbor, mode=mode))) vids.append(neighbor) parents.append(vid) added[neighbor] = True - break else: # No neighbor found, end of subtree stack.pop() diff --git a/tests/test_iterators.py b/tests/test_iterators.py index 7bd6d81bf..b8777d275 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -23,8 +23,8 @@ def testBFSIter(self): def testDFS(self): g = Graph.Tree(10, 2) vs, ps = g.dfs(0) - self.assertEqual(vs, [0, 1, 3, 7, 8, 4, 9, 2, 5, 6]) - self.assertEqual(ps, [0, 0, 1, 3, 3, 1, 4, 0, 2, 2]) + self.assertEqual(vs, [0, 2, 6, 5, 1, 4, 9, 3, 8, 7]) + self.assertEqual(ps, [0, 0, 2, 2, 0, 1, 4, 1, 3, 3]) def testDFSIter(self): g = Graph.Tree(10, 2) From ab6d22bc9a53022329c684a9d7890d5edec98a10 Mon Sep 17 00:00:00 2001 From: Yisu Remy Wang Date: Mon, 10 Aug 2020 12:50:06 -0700 Subject: [PATCH 0031/1681] Clarify Read_EdgeList behavior (#323) --- src/_igraph/graphobject.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 3709fc569..7585b4c3b 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14658,7 +14658,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS | METH_CLASS, "Read_Edgelist(f, directed=True)\n\n" "Reads an edge list from a file and creates a graph based on it.\n\n" - "Please note that the vertex indices are zero-based.\n\n" + "Please note that the vertex indices are zero-based. A vertex of zero\n" + "degree will be created for every integer that is in range but does not\n" + "appear in the edgelist.\n\n" "@param f: the name of the file or a Python file handle\n" "@param directed: whether the generated graph should be directed.\n"}, /* interface to igraph_read_graph_graphdb */ From 7c1ed6dfce8604105cf2e606f28b49277b29e7b4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 14 Aug 2020 00:05:38 +0200 Subject: [PATCH 0032/1681] clarified VertexClustering.giant() documentation, fixes #324 --- src/igraph/clustering.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 18660df93..8f519ddb1 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -422,13 +422,16 @@ def subgraphs(self): def giant(self): - """Returns the giant community of the clustered graph. + """Returns the largest cluster of the clustered graph. - The giant component a community for which no larger community exists. - @note: there can be multiple giant communities, this method will return - the copy of an arbitrary one if there are multiple giant communities. + The largest cluster is a cluster for which no larger cluster exists in + the clustering. It may also be known as the I{giant community} if the + clustering represents the result of a community detection function. - @return: a copy of the giant community. + @note: there can be multiple largest clusters, this method will return + the copy of an arbitrary one if there are multiple largest clusters. + + @return: a copy of the largest cluster. @precondition: the vertex set of the graph hasn't been modified since the moment the clustering was constructed. """ From 4e30ad1ce5864fbe15245bb44f275faef6ed646c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 14 Aug 2020 00:05:55 +0200 Subject: [PATCH 0033/1681] Python virtualenvs are typically called 'venv' or '.venv' so we use that in the docs --- doc/source/install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 45c26fbd9..6f067f093 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -44,8 +44,8 @@ Many users like to install packages into a project-specific `virtual environment `_. A variation of the following commands should work on most platforms: - $ python -m venv ENV - $ source ENV/bin/activate + $ python -m venv venv + $ source venv/bin/activate $ pip install python-igraph To test the installed package, launch Python within the virtual environment and run the From 2b5cf8a96b37e0080fede75f2294c5975e9b588f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 24 Aug 2020 19:10:19 +0200 Subject: [PATCH 0034/1681] fix incorrect handling of loop edges in graph difference --- tests/test_operators.py | 16 ++++++++++++++++ vendor/source/igraph | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_operators.py b/tests/test_operators.py index f539389f2..23b279140 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -22,6 +22,22 @@ def testIntersection(self): def testUnion(self): g = Graph.Tree(7, 2) | Graph.Lattice([7]) self.assertTrue(g.vcount() == 7 and g.ecount() == 12) + self.assertTrue(sorted(g.get_edgelist()) == [ + (0, 1), (0, 2), (0, 6), (1, 2), (1, 3), (1, 4), (2, 3), (2, 5), + (2, 6), (3, 4), (4, 5), (5, 6) + ]) + + def testDifference(self): + g = Graph.Tree(7, 2) - Graph.Lattice([7]) + self.assertTrue(g.vcount() == 7 and g.ecount() == 5) + self.assertTrue(sorted(g.get_edgelist()) == [(0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]) + + def testDifferenceWithSelfLoop(self): + # https://github.com/igraph/igraph/issues/597# + g = Graph.Ring(10) + [(0, 0)] + g -= Graph.Ring(5) + self.assertTrue(g.vcount() == 10 and g.ecount() == 7) + self.assertTrue(sorted(g.get_edgelist()) == [(0, 0), (0, 9), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]) def testInPlaceAddition(self): g = Graph.Full(3) diff --git a/vendor/source/igraph b/vendor/source/igraph index 4986fba9e..878de1310 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 4986fba9e43740acf2f02d081aea8dd221e2372c +Subproject commit 878de131016b23dc3b66d24e598c41325715b21f From bd3e5cbb4a0bfd9524beae399eb1d1c76a6f7216 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 2 Sep 2020 13:09:18 +0200 Subject: [PATCH 0035/1681] allow keyword arguments for Graph.complementer() --- src/_igraph/graphobject.c | 7 ++++--- src/_igraph/graphobject.h | 2 +- tests/test_operators.py | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 7585b4c3b..24cba807c 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -9717,13 +9717,14 @@ PyObject *igraphmodule_Graph_difference(igraphmodule_GraphObject * self, * \brief Creates the complementer of a graph */ PyObject *igraphmodule_Graph_complementer(igraphmodule_GraphObject * self, - PyObject * args) + PyObject * args, PyObject * kwds) { + static char *kwlist[] = { "loops", NULL }; igraphmodule_GraphObject *result; PyObject *o = Py_True; igraph_t g; - if (!PyArg_ParseTuple(args, "|O", &o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &o)) return NULL; if (igraph_complementer(&g, &self->g, PyObject_IsTrue(o))) { igraphmodule_handle_igraph_error(); @@ -15264,7 +15265,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // OPERATORS // /////////////// {"complementer", (PyCFunction) igraphmodule_Graph_complementer, - METH_VARARGS, + METH_VARARGS | METH_KEYWORDS, "complementer(loops=False)\n\n" "Returns the complementer of the graph\n\n" "@param loops: whether to include loop edges in the complementer.\n" diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 5c87becd6..3a00e0236 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -186,7 +186,7 @@ PyObject* igraphmodule_Graph_edge_attributes(igraphmodule_GraphObject* self); PyObject* igraphmodule_Graph_get_vertices(igraphmodule_GraphObject* self, void* closure); PyObject* igraphmodule_Graph_get_edges(igraphmodule_GraphObject* self, void* closure); -PyObject* igraphmodule_Graph_complementer(igraphmodule_GraphObject* self, PyObject* args); +PyObject* igraphmodule_Graph_complementer(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); PyObject* igraphmodule_Graph_complementer_op(igraphmodule_GraphObject* self); PyObject* igraphmodule_Graph_compose(igraphmodule_GraphObject* self, PyObject* other); PyObject* igraphmodule_Graph_difference(igraphmodule_GraphObject* self, PyObject* other); diff --git a/tests/test_operators.py b/tests/test_operators.py index 23b279140..7ea92ec00 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -10,6 +10,24 @@ np = None class OperatorTests(unittest.TestCase): + def testComplementer(self): + g = Graph.Full(3) + g2 = g.complementer() + self.assertTrue(g2.vcount() == 3 and g2.ecount() == 3) + self.assertTrue(sorted(g2.get_edgelist()) == [(0, 0), (1, 1), (2, 2)]) + + g = Graph.Full(3) + Graph.Full(2) + g2 = g.complementer(False) + self.assertTrue(sorted(g2.get_edgelist()) == [ + (0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4) + ]) + + g2 = g.complementer(loops=True) + self.assertTrue(sorted(g2.get_edgelist()) == [ + (0, 0), (0, 3), (0, 4), (1, 1), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), + (3, 3), (4, 4) + ]) + def testMultiplication(self): g = Graph.Full(3)*3 self.assertTrue(g.vcount() == 9 and g.ecount() == 9 From 88f2b3274403b68672a0ea316d23c39c4b5f3f5a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 2 Sep 2020 13:10:11 +0200 Subject: [PATCH 0036/1681] added Pandas to tox test environment because one of the unittests needs Pandas --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index e47224111..ffd3a411d 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ deps = scipy; platform_python_implementation != "PyPy" numpy; platform_python_implementation != "PyPy" networkx + pandas setenv = TESTING_IN_TOX=1 From 1b469ceff7105f16f618daba137f5c606fed03b3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 11 Sep 2020 10:41:12 +0200 Subject: [PATCH 0037/1681] added Graph.bridges(), closes #328 --- src/_igraph/graphobject.c | 35 +++++++++++++++++++++++++++++++++++ tests/test_structural.py | 8 ++++++++ 2 files changed, 43 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 24cba807c..8bed60901 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3970,6 +3970,32 @@ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject return Py_BuildValue("llll", (long)vcount1, (long)ecount1, (long)vcount2, (long)ecount2); } +/** \ingroup python_interface_graph + * \brief Calculates the bridges of a graph. + * \return the list of bridges in a PyObject + * \sa igraph_bridges + */ +PyObject *igraphmodule_Graph_bridges(igraphmodule_GraphObject *self) { + igraph_vector_t res; + PyObject *o; + if (igraph_vector_init(&res, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_bridges(&self->g, &res)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&res); + return NULL; + } + + igraph_vector_sort(&res); + o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(&res); + + return o; +} + /** \ingroup python_interface_graph * \brief Calculates the closeness centrality of some vertices in a graph. * \return the closeness centralities as a list (or a single float) @@ -12977,6 +13003,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Internal function, undocumented.\n\n" "@see: Graph.bipartite_projection_size()\n"}, + /* interface to igraph_bridges */ + {"bridges", (PyCFunction)igraphmodule_Graph_bridges, + METH_NOARGS, + "bridges()\n\n" + "Returns the list of bridges in the graph.\n\n" + "An edge is a bridge if its removal increases the number of (weakly) connected\n" + "components in the graph.\n" + }, + /* interface to igraph_closeness */ {"closeness", (PyCFunction) igraphmodule_Graph_closeness, METH_VARARGS | METH_KEYWORDS, diff --git a/tests/test_structural.py b/tests/test_structural.py index 22e6aedb1..2f192ce4c 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -375,6 +375,14 @@ def testNeighborhoodSize(self): class MiscTests(unittest.TestCase): + def testBridges(self): + g = Graph(5, [(0, 1), (1, 2), (2, 0), (0, 3), (3, 4)]) + self.assertTrue(g.bridges() == [3, 4]) + g = Graph(7, [(0, 1), (1, 2), (2, 0), (1, 6), (1, 3), (1, 4), (3, 5), (4, 5)]) + self.assertTrue(g.bridges() == [3]) + g = Graph(3, [(0, 1), (1, 2), (2, 3)]) + self.assertTrue(g.bridges() == [0, 1, 2]) + def testConstraint(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) self.assertTrue(isinstance(g.constraint(), list)) # TODO check more From 0da6bb668cc1e6dc976525d9e92da6a1d647e84d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 11 Sep 2020 12:49:31 +0200 Subject: [PATCH 0038/1681] exclude Pandas and NetworkX installation from pypy test run to prevent a timeout on the CI server --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index ffd3a411d..84fac76bb 100644 --- a/tox.ini +++ b/tox.ini @@ -11,8 +11,8 @@ commands = python -m unittest deps = scipy; platform_python_implementation != "PyPy" numpy; platform_python_implementation != "PyPy" - networkx - pandas + networkx; platform_python_implementation != "PyPy" + pandas; platform_python_implementation != "PyPy" setenv = TESTING_IN_TOX=1 From b7690ecf7b2632d48c5939f89890db0068b337eb Mon Sep 17 00:00:00 2001 From: deeenes Date: Mon, 14 Sep 2020 08:57:34 +0200 Subject: [PATCH 0039/1681] GraphSummary: vertex names can be not only str but something else that can be cast to str (#329) --- src/igraph/summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 69ef3eb36..143b597b0 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -152,7 +152,7 @@ def _construct_edgelist_adjlist(self): if self._graph.is_named(): names = self._graph.vs["name"] - maxlen = max(len(name) for name in names) + maxlen = max(len(str(name)) for name in names) format_str = "%%%ds %s %%s" % (maxlen, self._arrow) for v1, name in enumerate(names): neis = self._graph.successors(v1) From 1c9cf10aa6291d7d65ade5cf2a434e5a245b2a37 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 23 Sep 2020 19:54:21 +1000 Subject: [PATCH 0040/1681] Union/intersection of graphs with vertex names (#310) --- src/_igraph/convert.c | 35 +++ src/_igraph/convert.h | 2 + src/_igraph/graphobject.c | 213 +--------------- src/_igraph/graphobject.h | 3 +- src/_igraph/igraphmodule.c | 15 +- src/_igraph/operators.c | 316 ++++++++++++++++++++++++ src/_igraph/operators.h | 32 +++ src/igraph/__init__.py | 73 +++++- src/igraph/operators.py | 482 +++++++++++++++++++++++++++++++++++++ tests/test_generators.py | 8 +- tests/test_operators.py | 94 +++++++- 11 files changed, 1041 insertions(+), 232 deletions(-) create mode 100644 src/_igraph/operators.c create mode 100644 src/_igraph/operators.h create mode 100644 src/igraph/operators.py diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 947ba6789..1dcb2ec1f 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2345,6 +2345,41 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, return 0; } +/** + * \ingroup python_interface_conversion + * \brief Appends the contents of a Python iterator returning graphs to + * an \c igraph_vectorptr_t, and also stores the class of the first graph + * + * The incoming \c igraph_vector_ptr_t should be INITIALIZED. + * Raises suitable Python exceptions when needed. + * + * \param it the Python iterator + * \param v the \c igraph_vector_ptr_t which will contain the result + * \return 0 if everything was OK, 1 otherwise + */ +int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, + igraph_vector_ptr_t *v, + PyTypeObject **g_type) { + PyObject *t; + int first = 1; + + while ((t=PyIter_Next(it))) { + if (!PyObject_TypeCheck(t, &igraphmodule_GraphType)) { + PyErr_SetString(PyExc_TypeError, "iterable argument must contain graphs"); + Py_DECREF(t); + return 1; + } + if (first) { + *g_type = Py_TYPE(t); + first = 0; + } + igraph_vector_ptr_push_back(v, &((igraphmodule_GraphObject*)t)->g); + Py_DECREF(t); + } + + return 0; +} + /** * \ingroup python_interface_conversion * \brief Tries to interpret a Python object as a single vertex ID diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 635ddb6a0..5385c1293 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -100,6 +100,8 @@ PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v); int igraphmodule_PyList_to_strvector_t(PyObject* v, igraph_strvector_t *result); int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, igraph_vector_ptr_t *v); +int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, + igraph_vector_ptr_t *v, PyTypeObject **g_type); int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph); int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, igraph_t *graph, igraph_bool_t *return_single, diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8bed60901..d39a35702 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -268,7 +268,8 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, * * The newly created instance (which will be a subtype of )\c igraph.Graph) * will take ownership of the given \c igraph_t. This function is not - * accessible from Python. + * accessible from Python, however it is in the header file for other C API + * functions to use. */ PyObject* igraphmodule_Graph_subclass_from_igraph_t( PyTypeObject* type, igraph_t *graph @@ -9522,195 +9523,10 @@ PyObject *igraphmodule_Graph_edge_attributes(igraphmodule_GraphObject * self) } /********************************************************************** - * Graph operations (union, intersection etc) * + * Graph operations * + * Disjoint union, union and intersection are in operators.c * **********************************************************************/ -/** \ingroup python_interface_graph - * \brief Creates the disjoint union of two graphs (operator version) - */ -PyObject *igraphmodule_Graph_disjoint_union(igraphmodule_GraphObject * self, - PyObject * other) -{ - PyObject *it; - igraphmodule_GraphObject *o, *result; - igraph_t g; - - /* Did we receive an iterable? */ - it = PyObject_GetIter(other); - if (it) { - /* Get all elements, store the graphs in an igraph_vector_ptr */ - igraph_vector_ptr_t gs; - if (igraph_vector_ptr_init(&gs, 0)) { - Py_DECREF(it); - return igraphmodule_handle_igraph_error(); - } - if (igraph_vector_ptr_push_back(&gs, &self->g)) { - Py_DECREF(it); - igraph_vector_ptr_destroy(&gs); - return igraphmodule_handle_igraph_error(); - } - if (igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(it, &gs)) { - igraph_vector_ptr_destroy(&gs); - Py_DECREF(it); - return NULL; - } - Py_DECREF(it); - - /* Create disjoint union */ - if (igraph_disjoint_union_many(&g, &gs)) { - igraph_vector_ptr_destroy(&gs); - return igraphmodule_handle_igraph_error(); - } - - igraph_vector_ptr_destroy(&gs); - } else { - PyErr_Clear(); - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - o = (igraphmodule_GraphObject *) other; - - if (igraph_disjoint_union(&g, &self->g, &o->g)) { - igraphmodule_handle_igraph_error(); - return NULL; - } - } - - /* this is correct as long as attributes are not copied by the - * operator. if they are copied, the initialization should not empty - * the attribute hashes */ - CREATE_GRAPH(result, g); - - return (PyObject *) result; -} - -/** \ingroup python_interface_graph - * \brief Creates the union of two graphs (operator version) - */ -PyObject *igraphmodule_Graph_union(igraphmodule_GraphObject * self, - PyObject * other) -{ - PyObject *it; - igraphmodule_GraphObject *o, *result; - igraph_t g; - - /* Did we receive an iterable? */ - it = PyObject_GetIter(other); - if (it) { - /* Get all elements, store the graphs in an igraph_vector_ptr */ - igraph_vector_ptr_t gs; - if (igraph_vector_ptr_init(&gs, 0)) { - Py_DECREF(it); - return igraphmodule_handle_igraph_error(); - } - if (igraph_vector_ptr_push_back(&gs, &self->g)) { - Py_DECREF(it); - igraph_vector_ptr_destroy(&gs); - return igraphmodule_handle_igraph_error(); - } - if (igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(it, &gs)) { - Py_DECREF(it); - igraph_vector_ptr_destroy(&gs); - return NULL; - } - Py_DECREF(it); - - /* Create union */ - if (igraph_union_many(&g, &gs, /*edgemaps=*/ 0)) { - igraph_vector_ptr_destroy(&gs); - igraphmodule_handle_igraph_error(); - return NULL; - } - - igraph_vector_ptr_destroy(&gs); - } - else { - PyErr_Clear(); - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - o = (igraphmodule_GraphObject *) other; - - if (igraph_union(&g, &self->g, &o->g, /*edge_map1=*/ 0, - /*edge_map2=*/ 0)) { - igraphmodule_handle_igraph_error(); - return NULL; - } - } - - /* this is correct as long as attributes are not copied by the - * operator. if they are copied, the initialization should not empty - * the attribute hashes */ - CREATE_GRAPH(result, g); - - return (PyObject *) result; -} - -/** \ingroup python_interface_graph - * \brief Creates the intersection of two graphs (operator version) - */ -PyObject *igraphmodule_Graph_intersection(igraphmodule_GraphObject * self, - PyObject * other) -{ - PyObject *it; - igraphmodule_GraphObject *o, *result; - igraph_t g; - - /* Did we receive an iterable? */ - it = PyObject_GetIter(other); - if (it) { - /* Get all elements, store the graphs in an igraph_vector_ptr */ - igraph_vector_ptr_t gs; - if (igraph_vector_ptr_init(&gs, 0)) { - Py_DECREF(it); - return igraphmodule_handle_igraph_error(); - } - if (igraph_vector_ptr_push_back(&gs, &self->g)) { - Py_DECREF(it); - igraph_vector_ptr_destroy(&gs); - return igraphmodule_handle_igraph_error(); - } - if (igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(it, &gs)) { - Py_DECREF(it); - igraph_vector_ptr_destroy(&gs); - return NULL; - } - Py_DECREF(it); - - /* Create union */ - if (igraph_intersection_many(&g, &gs, /*edgemaps=*/ 0)) { - igraph_vector_ptr_destroy(&gs); - igraphmodule_handle_igraph_error(); - return NULL; - } - - igraph_vector_ptr_destroy(&gs); - } - else { - PyErr_Clear(); - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - o = (igraphmodule_GraphObject *) other; - - if (igraph_intersection(&g, &self->g, &o->g, /*edge_map1=*/ 0, - /*edge_map2=*/ 0)) { - igraphmodule_handle_igraph_error(); - return NULL; - } - } - - /* this is correct as long as attributes are not copied by the - * operator. if they are copied, the initialization should not empty - * the attribute hashes */ - CREATE_GRAPH(result, g); - - return (PyObject *) result; -} - /** \ingroup python_interface_graph * \brief Creates the difference of two graphs (operator version) */ @@ -15310,23 +15126,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"difference", (PyCFunction) igraphmodule_Graph_difference, METH_O, "difference(other)\n\nSubtracts the given graph from the original"}, - {"disjoint_union", (PyCFunction) igraphmodule_Graph_disjoint_union, - METH_O, - "disjoint_union(graphs)\n\n" - "Creates the disjoint union of two (or more) graphs.\n\n" - "@param graphs: the list of graphs to be united with the current one.\n"}, - {"intersection", (PyCFunction) igraphmodule_Graph_intersection, - METH_O, - "intersection(graphs)\n\n" - "Creates the intersection of two (or more) graphs.\n\n" - "@param graphs: the list of graphs to be intersected with\n" - " the current one.\n"}, - {"union", (PyCFunction) igraphmodule_Graph_union, - METH_O, - "union(graphs)\n\n" - "Creates the union of two (or more) graphs.\n\n" - "@param graphs: the list of graphs to be united with\n" - " the current one.\n"}, /**********************/ /* DOMINATORS */ @@ -16064,9 +15863,9 @@ PyNumberMethods igraphmodule_Graph_as_number = { (unaryfunc) igraphmodule_Graph_complementer_op, /*nb_invert */ 0, /*nb_lshift */ 0, /*nb_rshift */ - (binaryfunc) igraphmodule_Graph_intersection, /*nb_and */ + 0, /*nb_and */ 0, /*nb_xor */ - (binaryfunc) igraphmodule_Graph_union, /*nb_or */ + 0, /*nb_or */ #ifndef IGRAPH_PYTHON3 0, /*nb_coerce */ #endif diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 3a00e0236..618dbb97b 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -55,6 +55,7 @@ int igraphmodule_Graph_clear(igraphmodule_GraphObject *self); int igraphmodule_Graph_traverse(igraphmodule_GraphObject *self, visitproc visit, void *arg); void igraphmodule_Graph_dealloc(igraphmodule_GraphObject* self); int igraphmodule_Graph_init(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); +PyObject* igraphmodule_Graph_subclass_from_igraph_t(PyTypeObject* type, igraph_t *graph); PyObject* igraphmodule_Graph_from_igraph_t(igraph_t *graph); PyObject* igraphmodule_Graph_str(igraphmodule_GraphObject *self); @@ -191,8 +192,6 @@ PyObject* igraphmodule_Graph_complementer_op(igraphmodule_GraphObject* self); PyObject* igraphmodule_Graph_compose(igraphmodule_GraphObject* self, PyObject* other); PyObject* igraphmodule_Graph_difference(igraphmodule_GraphObject* self, PyObject* other); PyObject* igraphmodule_Graph_disjoint_union(igraphmodule_GraphObject* self, PyObject* other); -PyObject* igraphmodule_Graph_intersection(igraphmodule_GraphObject* self, PyObject* other); -PyObject* igraphmodule_Graph_union(igraphmodule_GraphObject* self, PyObject* other); PyObject* igraphmodule_Graph_bfs(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); PyObject* igraphmodule_Graph_bfsiter(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index fcd79063e..5e671486a 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -37,6 +37,7 @@ #include "random.h" #include "vertexobject.h" #include "vertexseqobject.h" +#include "operators.h" #define IGRAPH_MODULE #include "igraphmodule_api.h" @@ -651,6 +652,18 @@ static PyMethodDef igraphmodule_methods[] = METH_VARARGS | METH_KEYWORDS, "_split_join_distance(comm1, comm2)" }, + {"_disjoint_union", (PyCFunction)igraphmodule__disjoint_union, + METH_VARARGS | METH_KEYWORDS, + "_disjoint_union(graphs)" + }, + {"_union", (PyCFunction)igraphmodule__union, + METH_VARARGS | METH_KEYWORDS, + "_union(graphs, edgemaps)" + }, + {"_intersection", (PyCFunction)igraphmodule__intersection, + METH_VARARGS | METH_KEYWORDS, + "_intersection(graphs, edgemaps)" + }, {NULL, NULL, 0, NULL} }; @@ -658,7 +671,7 @@ static PyMethodDef igraphmodule_methods[] = "Low-level Python interface for the igraph library. " \ "Should not be used directly.\n\n" \ "@undocumented: community_to_membership, _compare_communities, _power_law_fit, " \ - "_split_join_distance" + "_split_join_distance, _union, _intersection, _disjoint_union" /** * Module definition table (only for Python 3.x) diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c new file mode 100644 index 000000000..3de9d4c15 --- /dev/null +++ b/src/_igraph/operators.c @@ -0,0 +1,316 @@ +/* vim:set ts=4 sw=2 sts=2 et: */ +/* + IGraph library. + Copyright (C) 2006-2012 Tamas Nepusz + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + +*/ + +#include "common.h" +#include "convert.h" +#include "error.h" +#include "graphobject.h" + + +/** \ingroup python_interface_graph + * \brief Creates the disjoint union of two or more graphs + */ +PyObject *igraphmodule__disjoint_union(PyObject *self, + PyObject *args, PyObject *kwds) +{ + static char* kwlist[] = { "graphs", NULL }; + PyObject *it, *graphs; + long int no_of_graphs; + igraph_vector_ptr_t gs; + igraphmodule_GraphObject *o; + PyObject *result; + PyTypeObject *result_type; + igraph_t g; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, + &graphs)) + return NULL; + + /* Needs to be an iterable */ + it = PyObject_GetIter(graphs); + if (!it) { + Py_DECREF(it); + return igraphmodule_handle_igraph_error(); + } + + /* Get all elements, store the graphs in an igraph_vector_ptr */ + if (igraph_vector_ptr_init(&gs, 0)) { + Py_DECREF(it); + return igraphmodule_handle_igraph_error(); + } + if (igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(it, &gs, &result_type)) { + Py_DECREF(it); + igraph_vector_ptr_destroy(&gs); + return NULL; + } + Py_DECREF(it); + no_of_graphs = (long int) igraph_vector_ptr_size(&gs); + + /* Create disjoint union */ + if (igraph_disjoint_union_many(&g, &gs)) { + igraph_vector_ptr_destroy(&gs); + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_ptr_destroy(&gs); + + /* this is correct as long as attributes are not copied by the + * operator. if they are copied, the initialization should not empty + * the attribute hashes */ + if (no_of_graphs > 0) { + result = igraphmodule_Graph_subclass_from_igraph_t( + result_type, + &g); + } + else { + result = igraphmodule_Graph_from_igraph_t(&g); + } + + return result; +} + + +/** \ingroup python_interface_graph + * \brief Creates the union of two or more graphs + */ +PyObject *igraphmodule__union(PyObject *self, + PyObject *args, PyObject *kwds) +{ + static char* kwlist[] = { "graphs", "edgemaps", NULL }; + PyObject *it, *em_list, *graphs, *with_edgemaps_o; + int with_edgemaps = 0; + long int no_of_graphs; + igraph_vector_ptr_t gs; + igraphmodule_GraphObject *o; + PyObject *result; + PyTypeObject *result_type; + igraph_t g; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, + &graphs, &with_edgemaps_o)) + return NULL; + + if (PyObject_IsTrue(with_edgemaps_o)) + with_edgemaps = 1; + + /* Needs to be an iterable */ + it = PyObject_GetIter(graphs); + if (!it) { + Py_DECREF(it); + return igraphmodule_handle_igraph_error(); + } + + /* Get all elements, store the graphs in an igraph_vector_ptr */ + if (igraph_vector_ptr_init(&gs, 0)) { + Py_DECREF(it); + return igraphmodule_handle_igraph_error(); + } + if (igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(it, &gs, &result_type)) { + Py_DECREF(it); + igraph_vector_ptr_destroy(&gs); + return NULL; + } + Py_DECREF(it); + no_of_graphs = (long int) igraph_vector_ptr_size(&gs); + + if (with_edgemaps) { + /* prepare edgemaps */ + igraph_vector_ptr_t edgemaps; + if (igraph_vector_ptr_init(&edgemaps, 0)) { + return igraphmodule_handle_igraph_error(); + } + + /* Create union */ + if (igraph_union_many(&g, &gs, &edgemaps)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_ptr_destroy(&edgemaps); + igraphmodule_handle_igraph_error(); + return NULL; + } + + /* extract edgemaps */ + long int i; + em_list = PyList_New((Py_ssize_t) no_of_graphs); + for (i = 0; i < no_of_graphs; i++) { + long int j; + long int no_of_edges = (long int) igraph_ecount(VECTOR(gs)[i]); + igraph_vector_t *map = VECTOR(edgemaps)[i]; + PyObject *emi = PyList_New((Py_ssize_t) no_of_edges); + for (j = 0; j < no_of_edges; j++) { + PyObject *dest = PyLong_FromLong(VECTOR(*map)[j]); + PyList_SET_ITEM(emi, (Py_ssize_t) j, dest); + } + PyList_SET_ITEM(em_list, (Py_ssize_t) i, emi); + } + igraph_vector_ptr_destroy(&edgemaps); + } + else { + /* Create union */ + if (igraph_union_many(&g, &gs, /* edgemaps */ 0)) { + igraph_vector_ptr_destroy(&gs); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + igraph_vector_ptr_destroy(&gs); + + /* this is correct as long as attributes are not copied by the + * operator. if they are copied, the initialization should not empty + * the attribute hashes */ + if (no_of_graphs > 0) { + o = (igraphmodule_GraphObject*) igraphmodule_Graph_subclass_from_igraph_t( + result_type, + &g); + } + else { + o = (igraphmodule_GraphObject*) igraphmodule_Graph_from_igraph_t(&g); + } + + if (with_edgemaps) { + /* wrap in a dictionary */ + result = PyDict_New(); + PyDict_SetItemString(result, "graph", (PyObject *) o); + Py_DECREF(o); + PyDict_SetItemString(result, "edgemaps", em_list); + } + else { + result = (PyObject *) o; + } + + return result; +} + +/** \ingroup python_interface_graph + * \brief Creates the intersection of two or more graphs + */ +PyObject *igraphmodule__intersection(PyObject *self, + PyObject *args, PyObject *kwds) +{ + static char* kwlist[] = { "graphs", "edgemaps", NULL }; + PyObject *it, *em_list, *graphs, *with_edgemaps_o; + int with_edgemaps = 0; + long int no_of_graphs; + igraph_vector_ptr_t gs; + igraphmodule_GraphObject *o; + PyObject *result; + PyTypeObject *result_type; + igraph_t g; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, + &graphs, &with_edgemaps_o)) + return NULL; + + if (PyObject_IsTrue(with_edgemaps_o)) + with_edgemaps = 1; + + /* Needs to be an iterable */ + it = PyObject_GetIter(graphs); + if (!it) { + Py_DECREF(it); + return igraphmodule_handle_igraph_error(); + } + + /* Get all elements, store the graphs in an igraph_vector_ptr */ + if (igraph_vector_ptr_init(&gs, 0)) { + Py_DECREF(it); + return igraphmodule_handle_igraph_error(); + } + if (igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(it, &gs, &result_type)) { + Py_DECREF(it); + igraph_vector_ptr_destroy(&gs); + return NULL; + } + Py_DECREF(it); + no_of_graphs = (long int) igraph_vector_ptr_size(&gs); + + if (with_edgemaps) { + /* prepare edgemaps */ + igraph_vector_ptr_t edgemaps; + if (igraph_vector_ptr_init(&edgemaps, 0)) { + return igraphmodule_handle_igraph_error(); + } + + /* Create intersection */ + if (igraph_intersection_many(&g, &gs, &edgemaps)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_ptr_destroy(&edgemaps); + igraphmodule_handle_igraph_error(); + return NULL; + } + + long int i; + em_list = PyList_New((Py_ssize_t) no_of_graphs); + for (i = 0; i < no_of_graphs; i++) { + long int j; + long int no_of_edges = (long int) igraph_ecount(VECTOR(gs)[i]); + igraph_vector_t *map = VECTOR(edgemaps)[i]; + PyObject *emi = PyList_New((Py_ssize_t) no_of_edges); + for (j = 0; j < no_of_edges; j++) { + PyObject *dest = PyLong_FromLong(VECTOR(*map)[j]); + PyList_SET_ITEM(emi, (Py_ssize_t) j, dest); + } + PyList_SET_ITEM(em_list, (Py_ssize_t) i, emi); + } + igraph_vector_ptr_destroy(&edgemaps); + + } + else { + /* Create intersection */ + if (igraph_intersection_many(&g, &gs, /* edgemaps */ 0)) { + igraph_vector_ptr_destroy(&gs); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + + igraph_vector_ptr_destroy(&gs); + + /* this is correct as long as attributes are not copied by the + * operator. if they are copied, the initialization should not empty + * the attribute hashes */ + if (no_of_graphs > 0) { + o = (igraphmodule_GraphObject*) igraphmodule_Graph_subclass_from_igraph_t( + result_type, + &g); + } + else { + o = (igraphmodule_GraphObject*) igraphmodule_Graph_from_igraph_t(&g); + } + + if (with_edgemaps) { + /* wrap in a dictionary */ + result = PyDict_New(); + PyDict_SetItemString(result, "graph", (PyObject *) o); + Py_DECREF(o); + PyDict_SetItemString(result, "edgemaps", em_list); + Py_DECREF(em_list); + } + else { + result = (PyObject *) o; + } + + return result; +} + + diff --git a/src/_igraph/operators.h b/src/_igraph/operators.h new file mode 100644 index 000000000..3823a55ae --- /dev/null +++ b/src/_igraph/operators.h @@ -0,0 +1,32 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2006-2012 Tamas Nepusz + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + +*/ + +#ifndef PYTHON_OPERATORS_H +#define PYTHON_OPERATORS_H + +#include + +PyObject* igraphmodule__disjoint_union(PyObject* self, PyObject* args, PyObject* kwds); +PyObject* igraphmodule__union(PyObject* self, PyObject* args, PyObject* kwds); +PyObject* igraphmodule__intersection(PyObject* self, PyObject* args, PyObject* kwds); + +#endif diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 248754144..e61d97a40 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -41,6 +41,7 @@ from igraph.formula import * from igraph.layout import * from igraph.matching import * +from igraph.operators import * from igraph.statistics import * from igraph.summary import * from igraph.utils import * @@ -3088,23 +3089,26 @@ def DataFrame(klass, edges, directed=True, vertices=None): vertices = vertices.copy() vertices.iloc[:, 0].fillna('NA', inplace=True) - names = np.unique(edges.values[:, :2]) + names_edges = np.unique(edges.values[:, :2]) - if vertices is not None: - names_edges = names + if vertices is None: + names = names_edges + else: if vertices.shape[1] < 1: raise ValueError('vertices has no columns') - names = vertices.iloc[:, 0].astype(str) + names_vertices = vertices.iloc[:, 0].astype(str) - if names.duplicated().any(): + if names_vertices.duplicated().any(): raise ValueError('Vertex names must be unique') - if len(np.setdiff1d(names_edges, names.values)): + names_vertices = names_vertices.values + + if len(np.setdiff1d(names_edges, names_vertices)): raise ValueError( 'Some vertices in the edge DataFrame are missing from vertices DataFrame') - names = names.values + names = names_vertices # create graph g = Graph(n=len(names), directed=directed) @@ -3128,9 +3132,9 @@ def DataFrame(klass, edges, directed=True, vertices=None): # edge attributes if edges.shape[1] > 2: - for e, (_, attr) in zip(g.es, edges.iloc[:, 2:]): - for an, av in attr.items(): - e[an] = av + for e, (_, attr) in zip(g.es, edges.iloc[:, 2:].iterrows()): + for a_name, a_value in attr.items(): + e[a_name] = a_value return g @@ -3379,7 +3383,7 @@ def __isub__(self, other): elif isinstance(other, tuple) and len(other) == 2: self.delete_edges([other]) elif isinstance(other, list): - if len(other)>0: + if len(other) > 0: if isinstance(other[0], tuple): self.delete_edges(other) elif isinstance(other[0], (int, long, basestring)): @@ -3418,7 +3422,7 @@ def __sub__(self, other): elif isinstance(other, tuple) and len(other) == 2: result.delete_edges([other]) elif isinstance(other, list): - if len(other)>0: + if len(other) > 0: if isinstance(other[0], tuple): result.delete_edges(other) elif isinstance(other[0], (int, long, basestring)): @@ -3454,7 +3458,7 @@ def __mul__(self, other): elif other == 1: return self elif other > 1: - return self.disjoint_union([self]*(other-1)) + return self.disjoint_union([self] * (other - 1)) else: return NotImplemented @@ -3752,6 +3756,49 @@ def summary(self, verbosity=0, width=None, *args, **kwds): """ return str(GraphSummary(self, verbosity, width, *args, **kwds)) + def disjoint_union(self, other): + '''disjoint_union(self, other) + + Creates the disjoint union of two (or more) graphs. + + @param graphs: graph or list of graphs to be united with + the current one. + @return: the disjoint union graph + ''' + if isinstance(other, GraphBase): + other = [other] + return disjoint_union([self] + other) + + def union(self, other, byname='auto'): + '''union(self, other) + + Creates the union of two (or more) graphs. + + @param graphs: graph or list of graphs to be united with + the current one. + @param byname: whether to use vertex names instead of ids. See + L{igraph.union} for details. + @return: the union graph + ''' + if isinstance(other, GraphBase): + other = [other] + return union([self] + other, byname=byname) + + def intersection(self, other, byname='auto'): + '''intersection(self, other) + + Creates the intersection of two (or more) graphs. + + @param other: graph or list of graphs to be intersected with + the current one. + @param byname: whether to use vertex names instead of ids. See + L{igraph.intersection} for details. + @return: the intersection graph + ''' + if isinstance(other, GraphBase): + other = [other] + return intersection([self] + other, byname=byname) + _format_mapping = { "ncol": ("Read_Ncol", "write_ncol"), "lgl": ("Read_Lgl", "write_lgl"), diff --git a/src/igraph/operators.py b/src/igraph/operators.py new file mode 100644 index 000000000..947cd0809 --- /dev/null +++ b/src/igraph/operators.py @@ -0,0 +1,482 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +""" +IGraph library. + +@undocumented: deprecated, _graphmethod, _add_proxy_methods, _layout_method_wrapper, + _3d_version_for +""" + +from __future__ import with_statement + +__license__ = u""" +Copyright (C) 2006-2012 Tamás Nepusz +Pázmány Péter sétány 1/a, 1117 Budapest, Hungary + +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 2 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA +""" + +# pylint: disable-msg=W0401 +# W0401: wildcard import +from igraph._igraph import * +from igraph._igraph import _union, _intersection, _disjoint_union + +from collections import defaultdict, Counter +from warnings import warn + + +def disjoint_union(graphs): + """Graph disjoint union. + + The disjoint union of two or more graphs is created. + + This function keeps the attributes of all graphs. All graph, vertex and + edge attributes are copied to the result. If an attribute is present in + multiple graphs and would result a name clash, then this attribute is + renamed by adding suffixes: _1, _2, etc. + + An error is generated if some input graphs are directed and others are + undirected. + + @param graph: list of graphs. A lazy sequence is not acceptable. + @return: the disjoint union graph + """ + if any(not isinstance(g, GraphBase) for g in graphs): + raise TypeError('Not all elements are graphs') + + ngr = len(graphs) + # Trivial cases + if ngr == 0: + return None + if ngr == 1: + return graphs[0].copy() + # Now there are at least two graphs + + graph_union = _disjoint_union(graphs) + + # Graph attributes + # NOTE: a_first_graph tracks which graph has the 1st occurrence of an + # attribute, while a_conflict track attributes with naming conflicts + a_first_graph = {} + a_conflict = set() + for ig, g in enumerate(graphs, 1): + # NOTE: a_name is the name of the attribute, a_value its value + for a_name in g.attributes(): + a_value = g[a_name] + # No conflicts + if a_name not in graph_union.attributes(): + a_first_graph[a_name] = ig + graph_union[a_name] = a_value + continue + if graph_union[a_name] == a_value: + continue + if a_name not in a_conflict: + # New conflict + a_conflict.add(a_name) + igf = a_first_graph[a_name] + graph_union['{:}_{:}'.format(a_name, igf)] = \ + graph_union.pop(a_name) + graph_union['{:}_{:}'.format(a_name, ig)] = a_value + + # Vertex attributes + i = 0 + for g in graphs: + nv = g.vcount() + for attr in g.vertex_attributes(): + graph_union.vs[i: i + nv][attr] = g.vs[attr] + i += nv + + # Edge attributes + i = 0 + for g in graphs: + ne = g.ecount() + for attr in g.edge_attributes(): + graph_union.es[i: i + ne][attr] = g.es[attr] + i += ne + + return graph_union + + +def union(graphs, byname='auto'): + """Graph union. + + The union of two or more graphs is created. The graphs may have identical + or overlapping vertex sets. Edges which are included in at least one graph + will be part of the new graph. + + This function keeps the attributes of all graphs. All graph, vertex and + edge attributes are copied to the result. If an attribute is present in + multiple graphs and would result a name clash, then this attribute is + renamed by adding suffixes: _1, _2, etc. + + The 'name' vertex attribute is treated specially if the operation is + performed based on symbolic vertex names. In this case 'name' must be + present in all graphs, and it is not renamed in the result graph. + + An error is generated if some input graphs are directed and others are + undirected. + + @param graph: list of graphs. A lazy sequence is not acceptable. + @param byname: bool or 'auto' specifying the function behaviour with + respect to names vertices (i.e. vertices with the 'name' attribute). If + False, ignore vertex names. If True, merge vertices based on names. If + 'auto', use True if all graphs have named vertices and False otherwise + (in the latter case, a warning is generated too). + @return: the union graph + """ + + if any(not isinstance(g, GraphBase) for g in graphs): + raise TypeError('Not all elements are graphs') + + if byname not in (True, False, 'auto'): + raise ValueError('"byname" should be a bool or "auto"') + + ngr = len(graphs) + n_named = sum(g.is_named() for g in graphs) + if byname == 'auto': + byname = n_named == ngr + if n_named not in (0, ngr): + warn("Some, but not all graphs are named, not using vertex names") + elif byname and (n_named != ngr): + raise AttributeError("Some graphs are not named") + # Now we know that byname is only used is all graphs are named + + # Trivial cases + if ngr == 0: + return None + if ngr == 1: + return graphs[0].copy() + # Now there are at least two graphs + + if byname: + allnames = [g.vs['name'] for g in graphs] + uninames = list(set.union(*(set(vns) for vns in allnames))) + permutation_map = {x: i for i, x in enumerate(uninames)} + nve = len(uninames) + newgraphs = [] + for g, vertex_names in zip(graphs, allnames): + # Make a copy + ng = g.copy() + + # Add the missing vertices + v_missing = list(set(uninames) - set(vertex_names)) + ng.add_vertices(v_missing) + + # Reorder vertices to match uninames + # vertex k -> p[k] + permutation = [permutation_map[x] for x in ng.vs['name']] + ng = ng.permute_vertices(permutation) + + newgraphs.append(ng) + else: + newgraphs = graphs + + # If any graph has any edge attributes, we need edgemaps + edgemaps = any(len(g.edge_attributes()) for g in graphs) + res = _union(newgraphs, edgemaps) + if edgemaps: + graph_union = res['graph'] + edgemaps = res['edgemaps'] + else: + graph_union = res + + # Graph attributes + a_first_graph = {} + a_conflict = set() + for ig, g in enumerate(newgraphs, 1): + # NOTE: a_name is the name of the attribute, a_value its value + for a_name in g.attributes(): + a_value = g[a_name] + # No conflicts + if a_name not in graph_union.attributes(): + a_first_graph[a_name] = ig + graph_union[a_name] = a_value + continue + if graph_union[a_name] == a_value: + continue + if a_name not in a_conflict: + # New conflict + a_conflict.add(a_name) + igf = a_first_graph[a_name] + graph_union['{:}_{:}'.format(a_name, igf)] = \ + graph_union.pop(a_name) + graph_union['{:}_{:}'.format(a_name, ig)] = a_value + + # Vertex attributes + if byname: + graph_union.vs['name'] = uninames + attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - set(['name']) + nve = graph_union.vcount() + for a_name in attrs: + # Check for conflicts at at least one vertex + conflict = False + vals = [None for i in range(nve)] + for g in newgraphs: + if a_name in g.vertex_attributes(): + for i, a_value in enumerate(g.vs[a_name]): + if a_value is None: + continue + if vals[i] is None: + vals[i] = a_value + continue + if vals[i] != a_value: + conflict = True + break + if conflict: + break + + if not conflict: + graph_union.vs[a_name] = vals + continue + + # There is a conflict, name after the graph number + for ig, g in enumerate(newgraphs, 1): + if a_name in g.vertex_attributes(): + graph_union.vs['{:}_{:}'.format(a_name, ig)] = g.vs[a_name] + + # Edge attributes + if edgemaps: + attrs = set.union(*(set(g.edge_attributes()) for g in newgraphs)) + ne = graph_union.ecount() + for a_name in attrs: + # Check for conflicts at at least one edge + conflict = False + vals = [None for i in range(ne)] + for g, emap in zip(newgraphs, edgemaps): + if a_name not in g.edge_attributes(): + continue + for iu, a_value in zip(emap, g.es[a_name]): + if a_value is None: + continue + if vals[iu] is None: + vals[iu] = a_value + continue + if vals[iu] != a_value: + print(g, g.vs['name'], emap, a_value, iu, vals[iu]) + conflict = True + break + if conflict: + break + + if not conflict: + graph_union.es[a_name] = vals + continue + + # There is a conflict, name after the graph number + for ig, (g, emap) in enumerate(zip(newgraphs, edgemaps), 1): + if a_name not in g.edge_attributes(): + continue + # Pass through map + vals = [None for i in range(ne)] + for iu, a_value in zip(emap, g.es[a_name]): + vals[iu] = a_value + graph_union.es['{:}_{:}'.format(a_name, ig)] = vals + + return graph_union + + +def intersection(graphs, byname='auto', keep_all_vertices=True): + """Graph intersection. + + The intersection of two or more graphs is created. The graphs may have + identical or overlapping vertex sets. Edges which are included in all + graphs will be part of the new graph. + + This function keeps the attributes of all graphs. All graph, vertex and + edge attributes are copied to the result. If an attribute is present in + multiple graphs and would result a name clash, then this attribute is + renamed by adding suffixes: _1, _2, etc. + + The 'name' vertex attribute is treated specially if the operation is + performed based on symbolic vertex names. In this case 'name' must be + present in all graphs, and it is not renamed in the result graph. + + An error is generated if some input graphs are directed and others are + undirected. + + @param graph: list of graphs. A lazy sequence is not acceptable. + @param byname: bool or 'auto' specifying the function behaviour with + respect to names vertices (i.e. vertices with the 'name' attribute). If + False, ignore vertex names. If True, merge vertices based on names. If + 'auto', use True if all graphs have named vertices and False otherwise + (in the latter case, a warning is generated too). + @keep_all_vertices: bool specifying if vertices that are not present in all + graphs should be kept in the intersection. + @return: the intersection graph + """ + + if any(not isinstance(g, GraphBase) for g in graphs): + raise TypeError('Not all elements are graphs') + + if byname not in (True, False, 'auto'): + raise ValueError('"byname" should be a bool or "auto"') + + ngr = len(graphs) + n_named = sum(g.is_named() for g in graphs) + if byname == 'auto': + byname = n_named == ngr + if n_named not in (0, ngr): + warn("Some, but not all graphs are named, not using vertex names") + elif byname and (n_named != ngr): + raise AttributeError("Some graphs are not named") + # Now we know that byname is only used is all graphs are named + + # Trivial cases + if ngr == 0: + return None + if ngr == 1: + return graphs[0].copy() + # Now there are at least two graphs + + if byname: + allnames = [g.vs['name'] for g in graphs] + + if keep_all_vertices: + uninames = list(set.union(*(set(vns) for vns in allnames))) + else: + uninames = list(set.intersection(*(set(vns) for vns in allnames))) + permutation_map = {x: i for i, x in enumerate(uninames)} + + nv = len(uninames) + newgraphs = [] + for g, vertex_names in zip(graphs, allnames): + # Make a copy + ng = g.copy() + + if keep_all_vertices: + # Add the missing vertices + v_missing = list(set(uninames) - set(vertex_names)) + ng.add_vertices(v_missing) + else: + # Delete the private vertices + v_private = list(set(vertex_names) - set(uninames)) + ng.delete_vertices(v_private) + + # Reorder vertices to match uninames + # vertex k -> p[k] + permutation = [permutation_map[x] for x in ng.vs['name']] + ng = ng.permute_vertices(permutation) + + newgraphs.append(ng) + else: + newgraphs = graphs + + # If any graph has any edge attributes, we need edgemaps + edgemaps = any(len(g.edge_attributes()) for g in graphs) + res = _intersection(newgraphs, edgemaps) + if edgemaps: + graph_intsec = res['graph'] + edgemaps = res['edgemaps'] + else: + graph_intsec = res + + # Graph attributes + a_first_graph = {} + a_conflict = set() + for ig, g in enumerate(newgraphs, 1): + # NOTE: a_name is the name of the attribute, a_value its value + for a_name in g.attributes(): + a_value = g[a_name] + # No conflicts + if a_name not in graph_intsec.attributes(): + a_first_graph[a_name] = ig + graph_intsec[a_name] = a_value + continue + if graph_intsec[a_name] == a_value: + continue + if a_name not in a_conflict: + # New conflict + a_conflict.add(a_name) + igf = a_first_graph[a_name] + graph_intsec['{:}_{:}'.format(a_name, igf)] = \ + graph_intsec.pop(a_name) + graph_intsec['{:}_{:}'.format(a_name, ig)] = a_value + + # Vertex attributes + if byname: + graph_intsec.vs['name'] = uninames + attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - set(['name']) + nv = graph_intsec.vcount() + for a_name in attrs: + # Check for conflicts at at least one vertex + conflict = False + vals = [None for i in range(nv)] + for g in newgraphs: + if a_name not in g.vertex_attributes(): + continue + for i, a_value in enumerate(g.vs[a_name]): + if a_value is None: + continue + if vals[i] is None: + vals[i] = a_value + continue + if vals[i] != a_value: + conflict = True + break + if conflict: + break + + if not conflict: + graph_intsec.vs[a_name] = vals + continue + + # There is a conflict, name after the graph number + for ig, g in enumerate(newgraphs, 1): + if a_name in g.vertex_attributes(): + graph_intsec.vs['{:}_{:}'.format(a_name, ig)] = g.vs[a_name] + + # Edge attributes + if edgemaps: + attrs = set.union(*(set(g.edge_attributes()) for g in newgraphs)) + ne = graph_intsec.ecount() + for a_name in attrs: + # Check for conflicts at at least one edge + conflict = False + vals = [None for i in range(ne)] + for g, emap in zip(newgraphs, edgemaps): + if a_name not in g.edge_attributes(): + continue + for iu, a_value in zip(emap, g.es[a_name]): + if iu == -1: + continue + if a_value is None: + continue + if vals[iu] is None: + vals[iu] = a_value + continue + if vals[iu] != a_value: + conflict = True + break + if conflict: + break + + if not conflict: + graph_intsec.es[a_name] = vals + continue + + # There is a conflict, name after the graph number + for ig, (g, emap) in enumerate(zip(newgraphs, edgemaps), 1): + if a_name not in g.edge_attributes(): + continue + # Pass through map + vals = [None for i in range(ne)] + for iu, a_value in zip(emap, g.es[a_name]): + if iu == -1: + continue + vals[iu] = a_value + graph_intsec.es['{:}_{:}'.format(a_name, ig)] = vals + + return graph_intsec diff --git a/tests/test_generators.py b/tests/test_generators.py index a8e20d403..5dad1405d 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -179,7 +179,7 @@ def testDataFrame(self): columns=[0, 1, 'weight']) g = Graph.DataFrame(edges, directed=False) - self.assertTrue(g.es["weight"] == [0.1, 0.4]) + self.assertTrue(g.es["weight"] == [0.4, 0.1]) vertices = pd.DataFrame( [['A', 'blue'], ['B', 'yellow'], ['C', 'blue']], @@ -190,12 +190,6 @@ def testDataFrame(self): self.assertTrue(g.vs["color"] == ['blue', 'yellow', 'blue']) self.assertTrue(g.es["weight"] == [0.4, 0.1]) - vertices.iloc[0, 0] = np.nan - g = Graph.DataFrame(edges, directed=True, vertices=vertices) - self.assertTrue(g.vs['name'] == ['NA', 'B', 'C']) - self.assertTrue(g.vs["color"] == ['blue', 'yellow', 'blue']) - self.assertTrue(g.es["weight"] == [0.4, 0.1]) - def suite(): generator_suite = unittest.makeSuite(GeneratorTests) diff --git a/tests/test_operators.py b/tests/test_operators.py index 7ea92ec00..064d4215d 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -29,14 +29,30 @@ def testComplementer(self): ]) def testMultiplication(self): - g = Graph.Full(3)*3 + g = Graph.Full(3) * 3 self.assertTrue(g.vcount() == 9 and g.ecount() == 9 - and g.clusters().membership == [0,0,0,1,1,1,2,2,2]) + and g.clusters().membership == [0, 0, 0, 1, 1, 1, 2, 2, 2]) def testIntersection(self): g = Graph.Tree(7, 2) & Graph.Lattice([7]) self.assertTrue(g.get_edgelist() == [(0, 1)]) + def testIntersectionMethod(self): + g = Graph.Tree(7, 2).intersection(Graph.Lattice([7])) + self.assertTrue(g.get_edgelist() == [(0, 1)]) + + def testDisjointUnion(self): + g1 = Graph.Tree(7, 2) + g2 = Graph.Lattice([7]) + + # Method + g = g1.disjoint_union(g2) + self.assertTrue(g.vcount() == 14 and g.ecount() == 13) + + # Module function + g = disjoint_union([g1, g2]) + self.assertTrue(g.vcount() == 14 and g.ecount() == 13) + def testUnion(self): g = Graph.Tree(7, 2) | Graph.Lattice([7]) self.assertTrue(g.vcount() == 7 and g.ecount() == 12) @@ -57,6 +73,80 @@ def testDifferenceWithSelfLoop(self): self.assertTrue(g.vcount() == 10 and g.ecount() == 7) self.assertTrue(sorted(g.get_edgelist()) == [(0, 0), (0, 9), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]) + def testUnionMethod(self): + g = Graph.Tree(7, 2).union(Graph.Lattice([7])) + self.assertTrue(g.vcount() == 7 and g.ecount() == 12) + + def testIntersectionMany(self): + gs = [Graph.Tree(7, 2), Graph.Lattice([7])] + g = intersection(gs) + self.assertTrue(g.get_edgelist() == [(0, 1)]) + + def testIntersectionManyAttributes(self): + gs = [Graph.Tree(7, 2), Graph.Lattice([7])] + gs[0]['attr'] = 'graph1' + gs[0].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] + gs[1].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] + gs[0].vs[0]['attr'] = 'set' + gs[1].vs[5]['attr'] = 'set_too' + g = intersection(gs) + names = g.vs['name'] + self.assertTrue(g['attr'] == 'graph1') + self.assertTrue(g.vs[names.index('one')]['attr'] == 'set') + self.assertTrue(g.vs[names.index('six')]['attr'] == 'set_too') + self.assertTrue(g.ecount() == 1) + self.assertTrue( + set(g.get_edgelist()[0]) == set([names.index('one'), names.index('two')]), + ) + + def testIntersectionManyEdgemap(self): + gs = [ + Graph.Formula('A-B'), + Graph.Formula('A-B,C-D'), + ] + gs[0].es[0]['attr'] = 'set' + gs[1].es[1]['attr'] = 'set_too' + g = intersection(gs) + self.assertTrue(g.es['attr'] == ['set']) + + def testUnionMany(self): + gs = [Graph.Tree(7, 2), Graph.Lattice([7]), Graph.Lattice([7])] + g = union(gs) + self.assertTrue(g.vcount() == 7 and g.ecount() == 12) + + def testUnionManyAttributes(self): + gs = [ + Graph.Formula('A-B'), + Graph.Formula('A-B,C-D'), + ] + gs[0]['attr'] = 'graph1' + gs[0].vs['attr'] = ['set', 'set_too'] + gs[0].vs['attr2'] = ['set', 'set_too'] + gs[1].vs[0]['attr'] = 'set' + gs[1].vs[0]['attr2'] = 'conflict' + g = union(gs) + names = g.vs['name'] + self.assertTrue(g['attr'] == 'graph1') + self.assertTrue(g.vs[names.index('A')]['attr'] == 'set') + self.assertTrue(g.vs[names.index('B')]['attr'] == 'set_too') + self.assertTrue(g.ecount() == 2) + self.assertTrue(sorted(g.vertex_attributes()) == ['attr', 'attr2_1', 'attr2_2', 'name']) + + def testUnionManyEdgemap(self): + gs = [ + Graph.Formula('A-B'), + Graph.Formula('C-D, A-B'), + ] + gs[0].es[0]['attr'] = 'set' + gs[1].es[0]['attr'] = 'set_too' + g = union(gs) + for e in g.es: + vnames = [g.vs[e.source]['name'], g.vs[e.target]['name']] + if set(vnames) == set(['A', 'B']): + self.assertTrue(e['attr'] == 'set') + else: + self.assertTrue(e['attr'] == 'set_too') + def testInPlaceAddition(self): g = Graph.Full(3) orig = g From 38fbd84ff389e42cb540b7b88b08cf7468c2b0df Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 23 Sep 2020 12:17:27 +0200 Subject: [PATCH 0041/1681] union, intersection and disjoint_union operators now throw ValueError when given no graphs --- src/igraph/operators.py | 6 +- tests/test_operators.py | 128 +++++++++++++++++++++++++++------------- 2 files changed, 89 insertions(+), 45 deletions(-) diff --git a/src/igraph/operators.py b/src/igraph/operators.py index 947cd0809..469486dc0 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -60,7 +60,7 @@ def disjoint_union(graphs): ngr = len(graphs) # Trivial cases if ngr == 0: - return None + raise ValueError("disjoint_union() needs at least one graph") if ngr == 1: return graphs[0].copy() # Now there are at least two graphs @@ -156,7 +156,7 @@ def union(graphs, byname='auto'): # Trivial cases if ngr == 0: - return None + raise ValueError("union() needs at least one graph") if ngr == 1: return graphs[0].copy() # Now there are at least two graphs @@ -336,7 +336,7 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): # Trivial cases if ngr == 0: - return None + raise ValueError("intersection() needs at least one graph") if ngr == 1: return graphs[0].copy() # Now there are at least two graphs diff --git a/tests/test_operators.py b/tests/test_operators.py index 064d4215d..5261d4386 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -33,6 +33,18 @@ def testMultiplication(self): self.assertTrue(g.vcount() == 9 and g.ecount() == 9 and g.clusters().membership == [0, 0, 0, 1, 1, 1, 2, 2, 2]) + def testDifference(self): + g = Graph.Tree(7, 2) - Graph.Lattice([7]) + self.assertTrue(g.vcount() == 7 and g.ecount() == 5) + self.assertTrue(sorted(g.get_edgelist()) == [(0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]) + + def testDifferenceWithSelfLoop(self): + # https://github.com/igraph/igraph/issues/597# + g = Graph.Ring(10) + [(0, 0)] + g -= Graph.Ring(5) + self.assertTrue(g.vcount() == 10 and g.ecount() == 7) + self.assertTrue(sorted(g.get_edgelist()) == [(0, 0), (0, 9), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]) + def testIntersection(self): g = Graph.Tree(7, 2) & Graph.Lattice([7]) self.assertTrue(g.get_edgelist() == [(0, 1)]) @@ -41,6 +53,17 @@ def testIntersectionMethod(self): g = Graph.Tree(7, 2).intersection(Graph.Lattice([7])) self.assertTrue(g.get_edgelist() == [(0, 1)]) + def testIntersectionNoGraphs(self): + self.assertRaises(ValueError, intersection, []) + + def testIntersectionSingle(self): + g1 = Graph.Tree(7, 2) + g = intersection([g1]) + self.assertTrue(g != g1) + self.assertTrue(g.vcount() == g1.vcount() and g.ecount() == g1.ecount()) + self.assertTrue(g.is_directed() == g1.is_directed()) + self.assertTrue(g.get_edgelist() == g1.get_edgelist()) + def testDisjointUnion(self): g1 = Graph.Tree(7, 2) g2 = Graph.Lattice([7]) @@ -53,6 +76,17 @@ def testDisjointUnion(self): g = disjoint_union([g1, g2]) self.assertTrue(g.vcount() == 14 and g.ecount() == 13) + def testDisjointUnionNoGraphs(self): + self.assertRaises(ValueError, disjoint_union, []) + + def testDisjointUnionSingle(self): + g1 = Graph.Tree(7, 2) + g = disjoint_union([g1]) + self.assertTrue(g != g1) + self.assertTrue(g.vcount() == g1.vcount() and g.ecount() == g1.ecount()) + self.assertTrue(g.is_directed() == g1.is_directed()) + self.assertTrue(g.get_edgelist() == g1.get_edgelist()) + def testUnion(self): g = Graph.Tree(7, 2) | Graph.Lattice([7]) self.assertTrue(g.vcount() == 7 and g.ecount() == 12) @@ -61,53 +95,20 @@ def testUnion(self): (2, 6), (3, 4), (4, 5), (5, 6) ]) - def testDifference(self): - g = Graph.Tree(7, 2) - Graph.Lattice([7]) - self.assertTrue(g.vcount() == 7 and g.ecount() == 5) - self.assertTrue(sorted(g.get_edgelist()) == [(0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]) - - def testDifferenceWithSelfLoop(self): - # https://github.com/igraph/igraph/issues/597# - g = Graph.Ring(10) + [(0, 0)] - g -= Graph.Ring(5) - self.assertTrue(g.vcount() == 10 and g.ecount() == 7) - self.assertTrue(sorted(g.get_edgelist()) == [(0, 0), (0, 9), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]) - def testUnionMethod(self): g = Graph.Tree(7, 2).union(Graph.Lattice([7])) self.assertTrue(g.vcount() == 7 and g.ecount() == 12) - def testIntersectionMany(self): - gs = [Graph.Tree(7, 2), Graph.Lattice([7])] - g = intersection(gs) - self.assertTrue(g.get_edgelist() == [(0, 1)]) - - def testIntersectionManyAttributes(self): - gs = [Graph.Tree(7, 2), Graph.Lattice([7])] - gs[0]['attr'] = 'graph1' - gs[0].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] - gs[1].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] - gs[0].vs[0]['attr'] = 'set' - gs[1].vs[5]['attr'] = 'set_too' - g = intersection(gs) - names = g.vs['name'] - self.assertTrue(g['attr'] == 'graph1') - self.assertTrue(g.vs[names.index('one')]['attr'] == 'set') - self.assertTrue(g.vs[names.index('six')]['attr'] == 'set_too') - self.assertTrue(g.ecount() == 1) - self.assertTrue( - set(g.get_edgelist()[0]) == set([names.index('one'), names.index('two')]), - ) + def testUnionNoGraphs(self): + self.assertRaises(ValueError, union, []) - def testIntersectionManyEdgemap(self): - gs = [ - Graph.Formula('A-B'), - Graph.Formula('A-B,C-D'), - ] - gs[0].es[0]['attr'] = 'set' - gs[1].es[1]['attr'] = 'set_too' - g = intersection(gs) - self.assertTrue(g.es['attr'] == ['set']) + def testUnionSingle(self): + g1 = Graph.Tree(7, 2) + g = union([g1]) + self.assertTrue(g != g1) + self.assertTrue(g.vcount() == g1.vcount() and g.ecount() == g1.ecount()) + self.assertTrue(g.is_directed() == g1.is_directed()) + self.assertTrue(g.get_edgelist() == g1.get_edgelist()) def testUnionMany(self): gs = [Graph.Tree(7, 2), Graph.Lattice([7]), Graph.Lattice([7])] @@ -147,6 +148,49 @@ def testUnionManyEdgemap(self): else: self.assertTrue(e['attr'] == 'set_too') + def testIntersectionNoGraphs(self): + self.assertRaises(ValueError, intersection, []) + + def testIntersectionSingle(self): + g1 = Graph.Tree(7, 2) + g = intersection([g1]) + self.assertTrue(g != g1) + self.assertTrue(g.vcount() == g1.vcount() and g.ecount() == g1.ecount()) + self.assertTrue(g.is_directed() == g1.is_directed()) + self.assertTrue(g.get_edgelist() == g1.get_edgelist()) + + def testIntersectionMany(self): + gs = [Graph.Tree(7, 2), Graph.Lattice([7])] + g = intersection(gs) + self.assertTrue(g.get_edgelist() == [(0, 1)]) + + def testIntersectionManyAttributes(self): + gs = [Graph.Tree(7, 2), Graph.Lattice([7])] + gs[0]['attr'] = 'graph1' + gs[0].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] + gs[1].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] + gs[0].vs[0]['attr'] = 'set' + gs[1].vs[5]['attr'] = 'set_too' + g = intersection(gs) + names = g.vs['name'] + self.assertTrue(g['attr'] == 'graph1') + self.assertTrue(g.vs[names.index('one')]['attr'] == 'set') + self.assertTrue(g.vs[names.index('six')]['attr'] == 'set_too') + self.assertTrue(g.ecount() == 1) + self.assertTrue( + set(g.get_edgelist()[0]) == set([names.index('one'), names.index('two')]), + ) + + def testIntersectionManyEdgemap(self): + gs = [ + Graph.Formula('A-B'), + Graph.Formula('A-B,C-D'), + ] + gs[0].es[0]['attr'] = 'set' + gs[1].es[1]['attr'] = 'set_too' + g = intersection(gs) + self.assertTrue(g.es['attr'] == ['set']) + def testInPlaceAddition(self): g = Graph.Full(3) orig = g From a44e0e525261ff88981552fba89ab69a4fb68d98 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 23 Sep 2020 20:37:36 +1000 Subject: [PATCH 0042/1681] Clear function and improve delete vertices and edges (#320) --- src/_igraph/graphobject.c | 34 ++++++++++++++++++++++++++++------ src/igraph/__init__.py | 37 +++++++++++++++++++++++++------------ tests/test_basic.py | 19 +++++++++++++++++++ 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d39a35702..80b4afb12 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -567,10 +567,21 @@ PyObject *igraphmodule_Graph_add_vertices(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_delete_vertices(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list; + PyObject *list = 0; igraph_vs_t vs; - if (!PyArg_ParseTuple(args, "O", &list)) return NULL; + if (!PyArg_ParseTuple(args, "|O", &list)) return NULL; + + /* no arguments means delete all. */ + + /*Py_None also means all for now, but it is deprecated */ + if (list == Py_None) { + PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is " + "deprecated since igraph 0.8.3, please use " + "Graph.delete_vertices() instead"); + } + + /* this already converts no arguments and Py_None to all vertices */ if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, 0, 0)) return NULL; if (igraph_delete_vertices(&self->g, vs)) { @@ -631,13 +642,23 @@ PyObject *igraphmodule_Graph_add_edges(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_delete_edges(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list; + PyObject *list = 0; igraph_es_t es; static char *kwlist[] = { "edges", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &list)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &list)) return NULL; + /* no arguments means delete all. */ + + /*Py_None also means all for now, but it is deprecated */ + if (list == Py_None) { + PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is " + "deprecated since igraph 0.8.3, please use " + "Graph.delete_vertices() instead"); + } + + /* this already converts no arguments and Py_None to all vertices */ if (igraphmodule_PyObject_to_es_t(list, &es, &self->g, 0)) { /* something bad happened during conversion, return immediately */ return NULL; @@ -11910,7 +11931,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "delete_vertices(vs)\n\n" "Deletes vertices and all its edges from the graph.\n\n" "@param vs: a single vertex ID or the list of vertex IDs\n" - " to be deleted.\n"}, + " to be deleted. No argument deletes all vertices.\n"}, /* interface to igraph_add_edges */ {"add_edges", (PyCFunction) igraphmodule_Graph_add_edges, @@ -11929,7 +11950,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "All vertices will be kept, even if they lose all their edges.\n" "Nonexistent edges will be silently ignored.\n\n" "@param es: the list of edges to be removed. Edges are identifed by\n" - " edge IDs. L{EdgeSeq} objects are also accepted here.\n"}, + " edge IDs. L{EdgeSeq} objects are also accepted here. No argument\n" + " deletes all edges.\n"}, /* interface to igraph_degree */ {"degree", (PyCFunction) igraphmodule_Graph_degree, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index e61d97a40..364ae550d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -383,30 +383,32 @@ def delete_edges(self, *args, **kwds): """Deletes some edges from the graph. The set of edges to be deleted is determined by the positional and - keyword arguments. If any keyword argument is present, or the - first positional argument is callable, an edge - sequence is derived by calling L{EdgeSeq.select} with the same - positional and keyword arguments. Edges in the derived edge sequence - will be removed. Otherwise the first positional argument is considered - as follows: - - - C{None} - deletes all edges + keyword arguments. If the function is called without any arguments, + all edges are deleted. If any keyword argument is present, or the + first positional argument is callable, an edge sequence is derived by + calling L{EdgeSeq.select} with the same positional and keyword + arguments. Edges in the derived edge sequence will be removed. + Otherwise the first positional argument is considered as follows: + + - C{None} - deletes all edges (deprecated since 0.8.3) - a single integer - deletes the edge with the given ID - a list of integers - deletes the edges denoted by the given IDs - a list of 2-tuples - deletes the edges denoted by the given source-target vertex pairs. When multiple edges are present between a given source-target vertex pair, only one is removed. + + @deprecated: L{Graph.delete_edges(None)} has been replaced by + L{Graph.delete_edges()} - with no arguments - since igraph 0.8.3. """ if len(args) == 0 and len(kwds) == 0: - raise ValueError("expected at least one argument") - if len(kwds)>0 or (hasattr(args[0], "__call__") and \ - not isinstance(args[0], EdgeSeq)): + return GraphBase.delete_edges(self) + + if len(kwds) > 0 or (callable(args[0]) and not isinstance(args[0], EdgeSeq)): edge_seq = self.es(*args, **kwds) else: edge_seq = args[0] return GraphBase.delete_edges(self, edge_seq) - def indegree(self, *args, **kwds): """Returns the in-degrees in a list. @@ -492,6 +494,17 @@ def biconnected_components(self, return_articulation_points=False): return clustering blocks = biconnected_components + def clear(self): + """clear() + + Clears the graph, deleting all vertices, edges, and attributes. + + @see: L{Graph.delete_vertices} and L{Graph.delete_edges}. + """ + self.delete_vertices() + for attr in self.attributes(): + del self[attr] + def cohesive_blocks(self): """cohesive_blocks() diff --git a/tests/test_basic.py b/tests/test_basic.py index 6deebd4d7..5368db09e 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -178,6 +178,10 @@ def testDeleteVertices(self): self.assertRaises(ValueError, g.delete_vertices, "no-such-vertex") self.assertRaises(InternalError, g.delete_vertices, 2) + # Delete all vertices + g.delete_vertices() + self.assertEqual(0, g.vcount()) + def testAddEdge(self): g = Graph() g.add_vertices(["spam", "bacon", "eggs", "ham"]) @@ -274,6 +278,21 @@ def testDeleteEdges(self): self.assertRaises(ValueError, g.delete_edges, [("A", "C")]) self.assertRaises(ValueError, g.delete_edges, [(0, 15)]) + # Delete all edges + g.delete_edges() + self.assertEqual(0, g.ecount()) + + def testClear(self): + g = Graph.Famous("petersen") + g["name"] = list("petersen") + + # Clearing the graph + g.clear() + + self.assertEqual(0, g.vcount()) + self.assertEqual(0, g.ecount()) + self.assertEqual([], g.attributes()) + def testGraphGetEid(self): g = Graph.Famous("petersen") g.vs["name"] = list("ABCDEFGHIJ") From 6c13eadf5841a0f72f8d3a843a77ad13d0a4f310 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 25 Sep 2020 17:52:53 +0200 Subject: [PATCH 0043/1681] removed unused variable [ci skip] --- src/_igraph/operators.c | 1 - vendor/source/igraph | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c index 3de9d4c15..7f82f2b35 100644 --- a/src/_igraph/operators.c +++ b/src/_igraph/operators.c @@ -36,7 +36,6 @@ PyObject *igraphmodule__disjoint_union(PyObject *self, PyObject *it, *graphs; long int no_of_graphs; igraph_vector_ptr_t gs; - igraphmodule_GraphObject *o; PyObject *result; PyTypeObject *result_type; igraph_t g; diff --git a/vendor/source/igraph b/vendor/source/igraph index 878de1310..21b37c4b7 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 878de131016b23dc3b66d24e598c41325715b21f +Subproject commit 21b37c4b786ae77736aca8f8dfa4017418b9d486 From 10c2579e6b3fa579950ed08037cbd88d4351103c Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Sat, 26 Sep 2020 10:12:07 +0200 Subject: [PATCH 0044/1681] community_leiden: corrected unitialized variable. (#332) --- src/_igraph/graphobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 80b4afb12..765f7c3be 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -11639,7 +11639,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, PyObject *edge_weights_o = Py_None; PyObject *node_weights_o = Py_None; PyObject *initial_membership_o = Py_None; - PyObject *res; + PyObject *res = Py_None; int error = 0, i; long int n_iterations = 2; From 0c8b85880f1af6f32e0ae139a7ceca256164ae1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Sat, 26 Sep 2020 13:29:32 +0200 Subject: [PATCH 0045/1681] Docs: fix broken link in tutorial --- doc/source/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 935029af9..b3ffe7752 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -65,7 +65,7 @@ The command-line startup script imports all of |igraph|'s methods and objects in namespace, so it is practically equivalent to ``from igraph import *``. The difference between the two approaches (apart from saving some typing) is that the command-line script checks whether you have any of Python's more advanced shells installed and uses that instead of the -standard Python shell. Currently the module looks for `IPython `_ and +standard Python shell. Currently the module looks for `IPython `_ and IDLE (the Tcl/Tk-based graphical shell supplied with Python). If neither IPython nor IDLE is installed, the startup script launches the default Python shell. You can also modify the order in which these shells are searched by tweaking |igraph|'s configuration file From 679943e2c415dcfa2067535b1b2b3363334ff06e Mon Sep 17 00:00:00 2001 From: kmankinen <22212710+kmankinen@users.noreply.github.com> Date: Thu, 1 Oct 2020 17:12:09 +0300 Subject: [PATCH 0046/1681] Adjacency list example to tutorial (#334) Co-authored-by: Katja Mankinen --- doc/source/tutorial.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index b3ffe7752..76acd1919 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -229,7 +229,7 @@ Let's do the same with a stochastic generator! IGRAPH U---- 100 516 -- + attr: x (v), y (v) -TODO: discuss what the ``+ attr`` line means. +where ``+ attr`` shows names of the attributes for vertices (v) and edges (e). :meth:`Graph.GRG` generates a geometric random graph: *n* points are chosen randomly and uniformly inside the unit square and pairs of points closer to each other than a predefined @@ -637,7 +637,14 @@ you look them up by names, the other one will be available only by its index. Treating a graph as an adjacency matrix ======================================= -TODO +Adjacency matrix is another way to form a graph. In adjacency matrix, rows and columns are labeled by graph vertices: the elements of the matrix indicate whether the vertices *i* and *j* have a common edge (*i, j*). +The adjacency matrix for the example graph is + +>>> g.get_adjacency() +Matrix([[0, 1, 1, 0, 0, 1, 0], [1, 0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 1, 1, 0], [0, 0, 1, 0, 1, 0, 1], [0, 0, 1, 1, 0, 0, 0], [1, 0, 1, 0, 0, 0, 1], [0, 0, 0, 1, 0, 1, 0]]) + +For example, Claire (``[1, 0, 0, 1, 1, 1, 0]``) is directly connected to Alice (who has vertex index 0), Dennis (index 3), +Esther (index 4), and Frank (index 5), but not to Bob (index 1) nor George (index 6). Layouts and plotting ==================== From a5815f4eeddbb3c026a47bd3bde71e12e092b623 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 1 Oct 2020 16:27:17 +0200 Subject: [PATCH 0047/1681] bumped vendor/source/igraph to fix a bug in the RT layout --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 21b37c4b7..137b56963 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 21b37c4b786ae77736aca8f8dfa4017418b9d486 +Subproject commit 137b569634432c96f6ee3d4f88e09cbaa1b3da58 From 9b0bdfae08d25bf4918a7172d02268247c123afe Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 2 Oct 2020 10:20:01 +0200 Subject: [PATCH 0048/1681] updated documentation for eigenvector centrality and the C core itself --- src/_igraph/graphobject.c | 15 +++++++++++++++ vendor/source/igraph | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 765f7c3be..8102a059d 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13107,6 +13107,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "eigenvector_centrality(directed=True, scale=True, weights=None, return_eigenvalue=False, arpack_options=None)\n\n" "Calculates the eigenvector centralities of the vertices in a graph.\n\n" + "Eigenvector centrality is a measure of the importance of a node in a\n" + "network. It assigns relative scores to all nodes in the network based\n" + "on the principle that connections from high-scoring nodes contribute\n" + "more to the score of the node in question than equal connections from\n" + "low-scoring nodes. In practice, the centralities are determined by calculating\n" + "eigenvector corresponding to the largest positive eigenvalue of the\n" + "adjacency matrix. In the undirected case, this function considers\n" + "the diagonal entries of the adjacency matrix to be twice the number of\n" + "self-loops on the corresponding vertex.\n\n" + "In the directed case, the left eigenvector of the adjacency matrix is\n" + "calculated. In other words, the centrality of a vertex is proportional\n" + "to the sum of centralities of vertices pointing to it.\n\n" + "Eigenvector centrality is meaningful only for connected graphs.\n" + "Graphs that are not connected should be decomposed into connected\n" + "components, and the eigenvector centrality calculated for each separately.\n\n" "@param directed: whether to consider edge directions in a directed\n" " graph. Ignored for undirected graphs.\n" "@param scale: whether to normalize the centralities so the largest\n" diff --git a/vendor/source/igraph b/vendor/source/igraph index 137b56963..3ed99a2e9 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 137b569634432c96f6ee3d4f88e09cbaa1b3da58 +Subproject commit 3ed99a2e9c9005258aadead548750cadcc2221f1 From bad92f64db77fc56cb738fd7a5bca2f1c7a5406c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 2 Oct 2020 14:03:11 +0200 Subject: [PATCH 0049/1681] updated igraph subrepo to 0.8.3 [ci skip] --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 3ed99a2e9..cafe1e4d4 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 3ed99a2e9c9005258aadead548750cadcc2221f1 +Subproject commit cafe1e4d447dded9f640fd4f14b7973bef045d62 From b2face64cda748dd6806bed617359bf641c87407 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Oct 2020 21:16:23 +0200 Subject: [PATCH 0050/1681] speed up biconnected_components() postprocessing, fixes #281 --- src/igraph/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 364ae550d..43696c7ed 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -481,11 +481,14 @@ def biconnected_components(self, return_articulation_points=False): trees = GraphBase.biconnected_components(self, False) clusters = [] - for tree in trees: - cluster = set() - for edge in self.es[tree]: - cluster.update(edge.tuple) - clusters.append(sorted(cluster)) + if trees: + edgelist = self.get_edgelist() + for tree in trees: + cluster = set() + for edge_id in tree: + cluster.update(edgelist[edge_id]) + clusters.append(sorted(cluster)) + clustering = VertexCover(self, clusters) if return_articulation_points: From 532f034500809d3f5c5f076027a4132a70fbb558 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Oct 2020 21:24:19 +0200 Subject: [PATCH 0051/1681] Appveyor: switch to an MSYS2 mirror because repo.msys2.org is down at the moment [ci skip] --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7e3ea6a77..b6e8f8d73 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -24,8 +24,8 @@ platform: install: # update msys2 keyring first - - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" - - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig" + - bash -lc "curl -O https://mirror.yandex.ru/mirrors/msys2/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" + - bash -lc "curl -O https://mirror.yandex.ru/mirrors/msys2/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig" - bash -lc "pacman --noconfirm -U msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" # update msys2 @@ -33,7 +33,7 @@ install: - bash -lc "pacman --noconfirm -Sy" - bash -lc "pacman --needed --noconfirm -S zstd" - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - - pip install cibuildwheel==1.1.0 + - pip install cibuildwheel==1.6.1 before_build: - git submodule update --init --recursive From e241981762d141b4b2661a71ad6e691dc5ccba6e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Oct 2020 21:24:39 +0200 Subject: [PATCH 0052/1681] drop Python 3.5, add Python 3.9 to follow Python's branch status --- .travis.yml | 6 +++--- README.md | 4 ++-- tox.ini | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index cf3a43bd0..3293b1cd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,10 @@ language: python python: - "2.7" - - "3.5" - "3.6" - "3.7" - "3.8" + - "3.9" - pypy3 addons: @@ -34,7 +34,7 @@ jobs: - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - CIBW_TEST_COMMAND="cd {project} && python -m unittest" install: - - sudo python -m pip install cibuildwheel==1.1.0 + - sudo python -m pip install cibuildwheel==1.6.1 script: - python setup.py sdist - python -m cibuildwheel --output-dir wheelhouse @@ -66,7 +66,7 @@ jobs: - CIBW_BEFORE_BUILD="python setup.py build_c_core" - CIBW_TEST_COMMAND="cd {project} && python -m unittest" install: - - python -m pip install cibuildwheel==1.1.0 + - python -m pip install cibuildwheel==1.6.1 script: - python -m cibuildwheel --output-dir wheelhouse before_deploy: *before_deploy_releases diff --git a/README.md b/README.md index f8b13693b..a084ce5e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Travis CI](https://img.shields.io/travis/igraph/python-igraph)](https://travis-ci.org/igraph/python-igraph) -[![PyPI pyversions](https://img.shields.io/badge/python-2.7%20%7C%203.5%20%7C%203.6%20%7C%203.7%20%7C%203.8-blue)](https://pypi.python.org/pypi/python-igraph) +[![PyPI pyversions](https://img.shields.io/badge/python-2.7%20%7C%203.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://pypi.python.org/pypi/python-igraph) [![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) Python interface for the igraph library @@ -155,7 +155,7 @@ faster than the first one as the C core does not need to be recompiled. We aim to keep up with the development cycle of Python and support all official Python versions that have not reached their end of life yet. Currently this -means that we support Python 3.5 to 3.8, inclusive. Please refer to [this +means that we support Python 3.6 to 3.9, inclusive. Please refer to [this page](https://devguide.python.org/#branchstatus) for the status of Python branches and let us know if you encounter problems with `python-igraph` on any of the non-EOL Python versions. diff --git a/tox.ini b/tox.ini index 84fac76bb..d6f330ab3 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38, pypy, pypy3 +envlist = py27, py36, py37, py38, py39, pypy, pypy3 [testenv] commands = python -m unittest From 656d8d7cb9f08bbe5f0b0b819073a3a2391c70f1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Oct 2020 21:32:46 +0200 Subject: [PATCH 0053/1681] Appveyor: revert to cibuildwheel 1.1.0 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b6e8f8d73..233d7fa61 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,7 @@ install: - bash -lc "pacman --noconfirm -Sy" - bash -lc "pacman --needed --noconfirm -S zstd" - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - - pip install cibuildwheel==1.6.1 + - pip install cibuildwheel==1.1.0 before_build: - git submodule update --init --recursive From 6263c44418e91fa2028e9a5287feecc69e716a7b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 6 Oct 2020 22:31:53 +0200 Subject: [PATCH 0054/1681] Travis: Python 3.9 is still referred to as 3.9-dev --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3293b1cd4..ca2f1f73c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ python: - "3.6" - "3.7" - "3.8" - - "3.9" + - "3.9-dev" - pypy3 addons: From 50a8846597698cd5c7ce4f0cba0835485a695919 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 09:46:33 +0200 Subject: [PATCH 0055/1681] Travis: don't install NumPy / SciPy on py39 because there are no wheels yet --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index d6f330ab3..b660866ff 100644 --- a/tox.ini +++ b/tox.ini @@ -19,6 +19,11 @@ setenv = [testenv:py27] commands = python -m unittest discover +[testenv:py39] +deps = + networkx + pandas + [flake8] max-line-length = 80 select = C,E,F,W,B,B950 From ed8b329b950b54b7403f28cbf21252c94eb7e06e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 09:49:22 +0200 Subject: [PATCH 0056/1681] Appveyor: it looks like cibuildwheel 1.6.1 requires Python 3 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 233d7fa61..867d6d70f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,7 @@ install: - bash -lc "pacman --noconfirm -Sy" - bash -lc "pacman --needed --noconfirm -S zstd" - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - - pip install cibuildwheel==1.1.0 + - pip3 install cibuildwheel==1.6.1 before_build: - git submodule update --init --recursive From 034fa38a633383e1ac630e8a8d35a1179f7c92ea Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 10:51:11 +0200 Subject: [PATCH 0057/1681] Appveyor: force using Python 3 --- appveyor.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 867d6d70f..b80edf18c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,6 +6,7 @@ environment: CIBW_BEFORE_BUILD: python setup.py build_c_core CIBW_TEST_COMMAND: "cd {project} && python -m unittest" IGRAPH_EXTRA_CONFIGURE_ARGS: "--disable-graphml" + PYTHON: "C:\\Python37" matrix: - CIBW_BUILD: "*-win32" @@ -33,7 +34,14 @@ install: - bash -lc "pacman --noconfirm -Sy" - bash -lc "pacman --needed --noconfirm -S zstd" - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - - pip3 install cibuildwheel==1.6.1 + + # prepare Python + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + - "python --version" + - "python -m pip install --upgrade pip" + + # install cibuildwheel + - pip install cibuildwheel==1.6.1 before_build: - git submodule update --init --recursive From aa7f8194c7ee128b699a8278ce6d62cc01f25747 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 12:15:16 +0200 Subject: [PATCH 0058/1681] Appveyor: don't build for Python 3.5, we dropped supporting it --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index b80edf18c..6464920c1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,12 +10,13 @@ environment: matrix: - CIBW_BUILD: "*-win32" - CIBW_SKIP: "cp27-*" + CIBW_SKIP: "cp27-* cp35-*" MSYSTEM: MINGW32 PATH: C:\msys64\usr\bin;C:\msys64\mingw32\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x86" - CIBW_BUILD: "*-win_amd64" + CIBW_SKIP: "cp27-* cp35-*" MSYSTEM: MINGW64 PATH: C:\msys64\usr\bin;C:\msys64\mingw64\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x64" From 55f69be8c78fc9cb8a8c80f061a634056c5a8847 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 12:16:56 +0200 Subject: [PATCH 0059/1681] Travis: don't install pandas either for py39 --- tox.ini | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b660866ff..055ea15f8 100644 --- a/tox.ini +++ b/tox.ini @@ -20,9 +20,10 @@ setenv = commands = python -m unittest discover [testenv:py39] +# py39 support is still sparse; most of the optional dependencies have no +# wheels for Python 3.9 yet so we install only those where they do deps = networkx - pandas [flake8] max-line-length = 80 From df696700444af9180d361fdb7f9cf1b2f615b173 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 14:50:32 +0200 Subject: [PATCH 0060/1681] Travis: trying to use Python 3 as well for cibuildwheel --- .travis.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ca2f1f73c..0525cceb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,10 +34,10 @@ jobs: - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - CIBW_TEST_COMMAND="cd {project} && python -m unittest" install: - - sudo python -m pip install cibuildwheel==1.6.1 + - sudo python3 -m pip install cibuildwheel==1.6.1 script: - python setup.py sdist - - python -m cibuildwheel --output-dir wheelhouse + - python3 -m cibuildwheel --output-dir wheelhouse before_deploy: &before_deploy_releases - git config --local user.name "ntamas" - git config --local user.email "ntamas@gmail.com" @@ -61,14 +61,15 @@ jobs: - stage: wheels os: osx + osx_image: xcode11.2 language: shell env: - CIBW_BEFORE_BUILD="python setup.py build_c_core" - CIBW_TEST_COMMAND="cd {project} && python -m unittest" install: - - python -m pip install cibuildwheel==1.6.1 + - python3 -m pip install cibuildwheel==1.6.1 script: - - python -m cibuildwheel --output-dir wheelhouse + - python3 -m cibuildwheel --output-dir wheelhouse before_deploy: *before_deploy_releases deploy: *deploy_releases From b6595e46189f0af5d1fc085ff2b3edd2e802dbb5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 16:15:38 +0200 Subject: [PATCH 0061/1681] Travis: install pip for Python 3 in the image --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 0525cceb8..000da9860 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,6 +34,7 @@ jobs: - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - CIBW_TEST_COMMAND="cd {project} && python -m unittest" install: + - wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - sudo python3 -m pip install cibuildwheel==1.6.1 script: - python setup.py sdist From fccab29c1742bc73d7b684121bb6ee5e48d04663 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 7 Oct 2020 17:29:44 +0200 Subject: [PATCH 0062/1681] Travis: make setup.py sdist quieter so we don't hit the log limits --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 000da9860..e9486e7c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ jobs: - wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - sudo python3 -m pip install cibuildwheel==1.6.1 script: - - python setup.py sdist + - python setup.py -q sdist - python3 -m cibuildwheel --output-dir wheelhouse before_deploy: &before_deploy_releases - git config --local user.name "ntamas" From b17676fb1267223a5ebbb740bdaf781d7602e466 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 07:35:39 +0200 Subject: [PATCH 0063/1681] Appveyor: repo.msys2.org works again --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6464920c1..a1cfd8375 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,8 +26,8 @@ platform: install: # update msys2 keyring first - - bash -lc "curl -O https://mirror.yandex.ru/mirrors/msys2/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" - - bash -lc "curl -O https://mirror.yandex.ru/mirrors/msys2/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig" + - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" + - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig" - bash -lc "pacman --noconfirm -U msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" # update msys2 From 81e0ff9b2138555b690e04fce59218fe49387907 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 07:45:22 +0200 Subject: [PATCH 0064/1681] enable silent rules during igraph compilation to make Travis output shorter --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8aa98e59d..511590b2f 100644 --- a/setup.py +++ b/setup.py @@ -300,7 +300,7 @@ def compile_in(self, build_folder, source_folder=None): os.chdir(build_folder) print("Configuring igraph...") - configure_args = ["--disable-tls"] + configure_args = ["--disable-tls", "--enable-silent-rules"] if "IGRAPH_EXTRA_CONFIGURE_ARGS" in os.environ: configure_args.extend(os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ")) retcode = subprocess.call( From 84d0505c7c1537ea54b1003c73493d6577387a69 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 12:13:28 +0200 Subject: [PATCH 0065/1681] bumped version to 0.8.3 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 5b83e8332..3a1483d4e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.8.2' +version = '0.8.3' # The full version, including alpha/beta/rc tags. -release = '0.8.2' +release = '0.8.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index b523d76db..aea240828 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 8, 2) +__version_info__ = (0, 8, 3) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 6f95052e809c92b0b0e2e374e202ed0ffa6fb5fa Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 15:20:44 +0200 Subject: [PATCH 0066/1681] igraph.operators module: declare explicitly what is being exported --- src/igraph/operators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/igraph/operators.py b/src/igraph/operators.py index 469486dc0..6d19075cc 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -37,6 +37,8 @@ from collections import defaultdict, Counter from warnings import warn +__all__ = ("disjoint_union", "union", "intersection") + def disjoint_union(graphs): """Graph disjoint union. From de7b34f04c2405bd89063764cfa2c47ef52289a4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 15:45:34 +0200 Subject: [PATCH 0067/1681] don't use a wildcard import in igraph.operators --- src/igraph/operators.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/igraph/operators.py b/src/igraph/operators.py index 6d19075cc..da5efe31f 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -29,10 +29,7 @@ 02110-1301 USA """ -# pylint: disable-msg=W0401 -# W0401: wildcard import -from igraph._igraph import * -from igraph._igraph import _union, _intersection, _disjoint_union +from igraph._igraph import GraphBase, _union, _intersection, _disjoint_union from collections import defaultdict, Counter from warnings import warn From c3379d26e550abc71d176c53e475b10b960f9fd6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 16:19:52 +0200 Subject: [PATCH 0068/1681] igraph.operators: fix documentation header for Epydoc --- src/igraph/operators.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/igraph/operators.py b/src/igraph/operators.py index da5efe31f..a9aee0014 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -1,14 +1,11 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- -""" -IGraph library. - -@undocumented: deprecated, _graphmethod, _add_proxy_methods, _layout_method_wrapper, - _3d_version_for -""" +"""Implementation of union, disjoint union and intersection operators.""" from __future__ import with_statement +__all__ = ("disjoint_union", "union", "intersection") +__docformat__ = "restructuredtext en" __license__ = u""" Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -34,8 +31,6 @@ from collections import defaultdict, Counter from warnings import warn -__all__ = ("disjoint_union", "union", "intersection") - def disjoint_union(graphs): """Graph disjoint union. From ab408676c15d38047d2768599c7ccfe027893385 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 16:44:11 +0200 Subject: [PATCH 0069/1681] added missing global.rst include in several doc chapters --- doc/source/analysis.rst | 2 ++ doc/source/generation.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index a4ab5ec74..8c15b4b83 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -1,3 +1,5 @@ +.. include:: include/global.rst + Graph analysis ============== |igraph| enables analysis of graphs/networks from simple operations such as adding and removing nodes to complex theoretical constructs such as community detection. Read the `API documentation`_ for details on each function and class. diff --git a/doc/source/generation.rst b/doc/source/generation.rst index a3f5b952e..991e43dfd 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -1,3 +1,5 @@ +.. include:: include/global.rst + Graph generation ================ From 64e1fecb1f881c0676cdc9177bc073bdc904acd7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 17:29:24 +0200 Subject: [PATCH 0070/1681] fixed several Epydoc warnings --- src/_igraph/graphobject.c | 2 +- src/igraph/__init__.py | 72 +++++++++++++++++++-------------------- src/igraph/operators.py | 4 +-- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8102a059d..1cc937635 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14800,7 +14800,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"isomorphic_bliss", (PyCFunction) igraphmodule_Graph_isomorphic_bliss, METH_VARARGS | METH_KEYWORDS, "isomorphic_bliss(other, return_mapping_12=False, return_mapping_21=False,\n" - " sh1=\"fm\", sh2=None)\n\n" + " sh1=\"fm\", sh2=None, color1=None, color2=None)\n\n" "Checks whether the graph is isomorphic to another graph, using the\n" "BLISS isomorphism algorithm.\n\n" "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 43696c7ed..8646d6d3c 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -267,7 +267,7 @@ def add_edges(self, es, attributes=None): @param es: the list of edges to be added. Every edge is represented with a tuple containing the vertex IDs or names of the two endpoints. Vertices are enumerated from zero. - @params attributes: dict of sequences, all of length equal to the + @param attributes: dict of sequences, all of length equal to the number of edges to be added, containing the attributes of the new edges. """ @@ -302,23 +302,23 @@ def add_vertex(self, name=None, **kwds): return vertex def add_vertices(self, n, attributes=None): - """add_vertices(n) + """add_vertices(n, attributes=None) Adds some vertices to the graph. + Note that if C{n} is a sequence of strings, indicating the names of the + new vertices, and attributes has a key C{name}, the two conflict. In + that case the attribute will be applied. + @param n: the number of vertices to be added, or the name of a single vertex to be added, or a sequence of strings, each corresponding to the name of a vertex to be added. Names will be assigned to the C{name} vertex attribute. - @params attributes: dict of sequences, all of length equal to the + @param attributes: dict of sequences, all of length equal to the number of vertices to be added, containing the attributes of the new vertices. If n is a string (so a single vertex is added), then the values of this dict are the attributes themselves, but if n=1 then they have to be lists of length 1. - - Note that if n is a sequence of strings, indicating the names of the - new vertices, and attributes has a key 'name', the two conflict. In - that case the attribute will be applied. """ if isinstance(n, basestring): # Adding a single vertex with a name @@ -639,13 +639,15 @@ def get_adjacency(self, type=GET_ADJACENCY_BOTH, attribute=None, \ return Matrix(data) def get_adjacency_sparse(self, attribute=None): - """Returns the adjacency matrix of a graph as scipy csr matrix. + """Returns the adjacency matrix of a graph as a SciPy CSR matrix. + @param attribute: if C{None}, returns the ordinary adjacency matrix. When the name of a valid edge attribute is given here, the matrix returned will contain the default value at the places where there is no edge or the value of the given attribute where there is an edge. - @return: the adjacency matrix as a L{scipy.sparse.csr_matrix}.""" + @return: the adjacency matrix as a C{scipy.sparse.csr_matrix}. + """ try: from scipy import sparse except ImportError: @@ -697,7 +699,7 @@ def get_adjedgelist(self, *args, **kwds): return self.get_inclist(*args, **kwds) def get_all_simple_paths(self, v, to=None, cutoff=-1, mode=OUT): - """get_all_simple_paths(v, to=None, mode=OUT) + """get_all_simple_paths(v, to=None, cutoff=-1, mode=OUT) Calculates all the simple paths from a given node to some other nodes (or all of them) in a graph. @@ -1869,27 +1871,27 @@ def to_graph_tool( edge_attributes=None): """Converts the graph to graph-tool - @param graph_attributes: dictionary of graph attributes to transfer. - Keys are attributes from the graph, values are data types (see - below). C{None} means no graph attributes are transferred. - @param vertex_attributes: dictionary of vertex attributes to transfer. - Keys are attributes from the vertices, values are data types (see - below). C{None} means no vertex attributes are transferred. - @param edge_attributes: dictionary of edge attributes to transfer. - Keys are attributes from the edges, values are data types (see - below). C{None} means no vertex attributes are transferred. - Data types: graph-tool only accepts specific data types. See the following web page for a list: https://graph-tool.skewed.de/static/doc/quickstart.html - NOTE: because of the restricted data types in graph-tool, vertex and + Note: because of the restricted data types in graph-tool, vertex and edge attributes require to be type-consistent across all vertices or edges. If you set the property for only some vertices/edges, the other will be tagged as None in python-igraph, so they can only be converted to graph-tool with the type 'object' and any other conversion will fail. + + @param graph_attributes: dictionary of graph attributes to transfer. + Keys are attributes from the graph, values are data types (see + below). C{None} means no graph attributes are transferred. + @param vertex_attributes: dictionary of vertex attributes to transfer. + Keys are attributes from the vertices, values are data types (see + below). C{None} means no vertex attributes are transferred. + @param edge_attributes: dictionary of edge attributes to transfer. + Keys are attributes from the edges, values are data types (see + below). C{None} means no vertex attributes are transferred. """ import graph_tool as gt @@ -3018,7 +3020,7 @@ def GRG(klass, n, radius, torus=False): @classmethod def Incidence(klass, *args, **kwds): - """Incidence(matrix, directed=False, mode=ALL, multiple=False) + """Incidence(matrix, directed=False, mode=ALL, multiple=False, weighted=None) Creates a bipartite graph from an incidence matrix. @@ -3074,7 +3076,7 @@ def Incidence(klass, *args, **kwds): @classmethod def DataFrame(klass, edges, directed=True, vertices=None): - """DataFrame(directed=True, vertices=None) + """DataFrame(edges, directed=True, vertices=None) Generates a graph from one or two dataframes. @@ -3773,44 +3775,42 @@ def summary(self, verbosity=0, width=None, *args, **kwds): return str(GraphSummary(self, verbosity, width, *args, **kwds)) def disjoint_union(self, other): - '''disjoint_union(self, other) + """disjoint_union(self, other) Creates the disjoint union of two (or more) graphs. - @param graphs: graph or list of graphs to be united with - the current one. + @param other: graph or list of graphs to be united with the current one. @return: the disjoint union graph - ''' + """ if isinstance(other, GraphBase): other = [other] return disjoint_union([self] + other) def union(self, other, byname='auto'): - '''union(self, other) + """union(self, other, byname="auto") Creates the union of two (or more) graphs. - @param graphs: graph or list of graphs to be united with - the current one. + @param other: graph or list of graphs to be united with the current one. @param byname: whether to use vertex names instead of ids. See - L{igraph.union} for details. + L{igraph.union} for details. @return: the union graph - ''' + """ if isinstance(other, GraphBase): other = [other] return union([self] + other, byname=byname) def intersection(self, other, byname='auto'): - '''intersection(self, other) + """intersection(self, other, byname="auto") Creates the intersection of two (or more) graphs. @param other: graph or list of graphs to be intersected with - the current one. + the current one. @param byname: whether to use vertex names instead of ids. See - L{igraph.intersection} for details. + L{igraph.intersection} for details. @return: the intersection graph - ''' + """ if isinstance(other, GraphBase): other = [other] return intersection([self] + other, byname=byname) diff --git a/src/igraph/operators.py b/src/igraph/operators.py index a9aee0014..00f8e0c13 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -307,8 +307,8 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): False, ignore vertex names. If True, merge vertices based on names. If 'auto', use True if all graphs have named vertices and False otherwise (in the latter case, a warning is generated too). - @keep_all_vertices: bool specifying if vertices that are not present in all - graphs should be kept in the intersection. + @param keep_all_vertices: bool specifying if vertices that are not present + in all graphs should be kept in the intersection. @return: the intersection graph """ From ea43a546b3463d1874af0ad907bbc55a200d7acf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 17:29:34 +0200 Subject: [PATCH 0071/1681] Travis: skip generating wheels for Python 3.5 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e9486e7c0..d4b4da247 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,7 @@ jobs: env: - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - CIBW_TEST_COMMAND="cd {project} && python -m unittest" + - CIBW_SKIP="cp35-*" install: - wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - sudo python3 -m pip install cibuildwheel==1.6.1 From a3b48ac4e1a55dfe218c68038031ebcddec9a19b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 8 Oct 2020 20:07:57 +0200 Subject: [PATCH 0072/1681] fix three more warnings in the documentation generation --- doc/source/generation.rst | 2 +- src/igraph/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 991e43dfd..2e689dd36 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -55,7 +55,7 @@ If you don't specify a format, |igraph| will try to figure it out or, if that fa From external libraries +++++++++++++++++++++++ -|igraph| can read from and write to `networkx`_ and `graph-tool`_ graph formats: +|igraph| can read from and write to `networkx` and `graph-tool` graph formats: >>> g = Graph.from_networkx(nwx) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 8646d6d3c..20e9e9cbd 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -260,7 +260,7 @@ def add_edge(self, source, target, **kwds): return edge def add_edges(self, es, attributes=None): - """add_edges(es) + """add_edges(es, attributes=None) Adds some edges to the graph. From 64eab5458ae7662707df43721e6d9d23b4f1ad18 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 30 Oct 2020 18:14:12 +0100 Subject: [PATCH 0073/1681] chore: reformatted setup.py with black [skip ci] --- setup.py | 93 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/setup.py b/setup.py index 511590b2f..c20468ccc 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -#usr/bin/env python +#!usr/bin/env python import os import platform @@ -29,6 +29,7 @@ import sysconfig from select import select +from time import sleep ########################################################################### @@ -143,7 +144,7 @@ def is_unix_like(platform=None): ) -def find_msvc_source_folder(folder = ".", requires_built=False): +def find_msvc_source_folder(folder=".", requires_built=False): """Finds the folder that contains the MSVC-specific source of igraph if there is any. Returns `None` if no such folder is found. Prints a warning if the choice is ambiguous. @@ -151,15 +152,15 @@ def find_msvc_source_folder(folder = ".", requires_built=False): all_msvc_dirs = glob.glob(os.path.join(folder, "igraph-*-msvc")) if len(all_msvc_dirs) > 0: if len(all_msvc_dirs) > 1: - print( - "More than one MSVC build directory (..\\..\\igraph-*-msvc) found!" - ) + print("More than one MSVC build directory (..\\..\\igraph-*-msvc) found!") print( "It could happen that setup.py uses the wrong one! Please remove all but the right one!\n\n" ) msvc_builddir = all_msvc_dirs[-1] - if requires_built and not os.path.exists(os.path.join(msvc_builddir, "Release")): + if requires_built and not os.path.exists( + os.path.join(msvc_builddir, "Release") + ): print( "There is no 'Release' dir in the MSVC build directory\n(%s)" % msvc_builddir @@ -179,19 +180,20 @@ def preprocess_fallback_config(): global LIBIGRAPH_FALLBACK_LIBRARY_DIRS global LIBIGRAPH_FALLBACK_LIBRARIES - if platform.system() == "Windows" and distutils.ccompiler.get_default_compiler() == "msvc": + if ( + platform.system() == "Windows" + and distutils.ccompiler.get_default_compiler() == "msvc" + ): # if this setup is run in the source checkout *and* the igraph msvc was build, # this code adds the right library and include dir - msvc_builddir = find_msvc_source_folder(os.path.join("..", ".."), requires_built=True) + msvc_builddir = find_msvc_source_folder( + os.path.join("..", ".."), requires_built=True + ) if msvc_builddir is not None: print("Using MSVC build dir: %s\n\n" % msvc_builddir) - LIBIGRAPH_FALLBACK_INCLUDE_DIRS = [ - os.path.join(msvc_builddir, "include") - ] - LIBIGRAPH_FALLBACK_LIBRARY_DIRS = [ - os.path.join(msvc_builddir, "Release") - ] + LIBIGRAPH_FALLBACK_INCLUDE_DIRS = [os.path.join(msvc_builddir, "include")] + LIBIGRAPH_FALLBACK_LIBRARY_DIRS = [os.path.join(msvc_builddir, "Release")] return True else: return False @@ -269,7 +271,7 @@ def compile_in(self, build_folder, source_folder=None): source_folder = os.path.abspath(source_folder) build_folder = os.path.abspath(build_folder) - + build_to_source_folder = os.path.relpath(source_folder, build_folder) cwd = os.getcwd() @@ -302,20 +304,24 @@ def compile_in(self, build_folder, source_folder=None): print("Configuring igraph...") configure_args = ["--disable-tls", "--enable-silent-rules"] if "IGRAPH_EXTRA_CONFIGURE_ARGS" in os.environ: - configure_args.extend(os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ")) + configure_args.extend( + os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ") + ) retcode = subprocess.call( "sh {0} {1}".format( - quote_path_for_shell(os.path.join(build_to_source_folder, "configure")), - " ".join(configure_args) + quote_path_for_shell( + os.path.join(build_to_source_folder, "configure") + ), + " ".join(configure_args), ), env=self.enhanced_env(CFLAGS="-fPIC", CXXFLAGS="-fPIC"), - shell=True + shell=True, ) if retcode: return False building_on_windows = building_on_windows_msvc() - + if building_on_windows: print("Creating Microsoft Visual Studio project...") retcode = subprocess.call("make msvc", shell=True) @@ -331,13 +337,17 @@ def compile_in(self, build_folder, source_folder=None): devenv = os.environ.get("DEVENV_EXECUTABLE") os.chdir(msvc_source) if devenv is None: - retcode = subprocess.call("devenv /upgrade igraph.vcproj", shell=True) + retcode = subprocess.call( + "devenv /upgrade igraph.vcproj", shell=True + ) else: retcode = subprocess.call([devenv, "/upgrade", "igraph.vcproj"]) if retcode: return False - retcode = subprocess.call("msbuild.exe igraph.vcxproj /p:configuration=Release") + retcode = subprocess.call( + "msbuild.exe igraph.vcxproj /p:configuration=Release" + ) else: retcode = subprocess.call("make", shell=True) @@ -368,7 +378,7 @@ def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries ): building_on_windows = building_on_windows_msvc() - + create_dir_unless_exists(install_folder) ensure_dir_does_not_exist(install_folder, "include") @@ -387,9 +397,7 @@ def copy_build_artifacts( msvc_builddir = find_msvc_source_folder(build_folder, requires_built=True) if msvc_builddir is not None: print("Using MSVC build dir: %s\n\n" % msvc_builddir) - for fname in glob.glob( - os.path.join(msvc_builddir, "Release", "*.lib") - ): + for fname in glob.glob(os.path.join(msvc_builddir, "Release", "*.lib")): shutil.copy(fname, os.path.join(install_folder, "lib")) else: print("Cannot find MSVC build dir in %s\n\n" % build_folder) @@ -497,7 +505,9 @@ def run(self): if buildcfg.use_pkgconfig: detected = buildcfg.detect_from_pkgconfig() if not detected: - print("Cannot find the C core of igraph on this system using pkg-config.") + print( + "Cannot find the C core of igraph on this system using pkg-config." + ) sys.exit(1) else: # Build the C core from the vendored igraph source @@ -510,10 +520,11 @@ def run(self): # Add any extra library paths if needed; this is needed for the # Appveyor CI build if "IGRAPH_EXTRA_LIBRARY_PATH" in os.environ: - buildcfg.library_dirs = list( - os.environ["IGRAPH_EXTRA_LIBRARY_PATH"].split(os.pathsep) - ) + buildcfg.library_dirs - + buildcfg.library_dirs = ( + list(os.environ["IGRAPH_EXTRA_LIBRARY_PATH"].split(os.pathsep)) + + buildcfg.library_dirs + ) + # Replaces library names with full paths to static libraries # where possible. libm.a is excluded because it caused problems # on Sabayon Linux where libm.a is probably not compiled with @@ -549,9 +560,6 @@ def sdist(self): command. """ from setuptools.command.sdist import sdist - from distutils.sysconfig import get_python_inc - - buildcfg = self class custom_sdist(sdist): def run(self): @@ -698,7 +706,7 @@ def replace_static_libraries(self, only=None, exclusions=None): """Replaces references to libraries with full paths to their static versions if the static version is to be found on the library path.""" building_on_windows = building_on_windows_msvc() - + if not building_on_windows and "stdc++" not in self.libraries: self.libraries.append("stdc++") @@ -718,7 +726,7 @@ def use_vendored_igraph(self): """Assumes that igraph is installed already in ``vendor/install/igraph`` and sets up the include and library paths and the library names accordingly.""" building_on_windows = building_on_windows_msvc() - + buildcfg.include_dirs = [os.path.join("vendor", "install", "igraph", "include")] buildcfg.library_dirs = [os.path.join("vendor", "install", "igraph", "lib")] if not buildcfg.static_extension: @@ -803,17 +811,10 @@ def use_educated_guess(self): author_email="ntamas@gmail.com", ext_modules=[igraph_extension], package_dir={"igraph": "src/igraph"}, - packages=[ - "igraph", - "igraph.app", - "igraph.drawing", - "igraph.remote" - ], + packages=["igraph", "igraph.app", "igraph.drawing", "igraph.remote"], scripts=["scripts/igraph"], install_requires=["texttable>=1.6.2"], - extras_require={ - "plotting": ["pycairo>=1.18.0"] - }, + extras_require={"plotting": ["pycairo>=1.18.0"]}, headers=headers, platforms="ALL", keywords=[ @@ -846,7 +847,7 @@ def use_educated_guess(self): cmdclass={ "build_c_core": buildcfg.build_c_core, # used by CI "build_ext": buildcfg.build_ext, - "sdist": buildcfg.sdist + "sdist": buildcfg.sdist, }, ) From d2c2efc270bf61daadc7aca9719ea8bcdb581ca8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 30 Oct 2020 18:22:59 +0100 Subject: [PATCH 0074/1681] feat: added empty stub for soon-to-be-added CMake builder for igraph 0.9 [skip ci] --- setup.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index c20468ccc..5d18b9ba1 100644 --- a/setup.py +++ b/setup.py @@ -255,9 +255,11 @@ def wait_for_keypress(seconds): ########################################################################### -class IgraphCCoreBuilder(object): +class IgraphCCoreAutotoolsBuilder(object): """Class responsible for downloading and building the C core of igraph - if it is not installed yet.""" + if it is not installed yet, assuming that the C core uses `configure.ac` + and its friends. This used to be the case before igraph 0.9. + """ def compile_in(self, build_folder, source_folder=None): """Compiles igraph from its source code in the given folder. @@ -422,6 +424,27 @@ def enhanced_env(**kwargs): return env +########################################################################### + + +class IgraphCCoreCMakeBuilder(object): + """Class responsible for downloading and building the C core of igraph + if it is not installed yet, assuming that the C core uses CMake as the + build tool. This is the case from igraph 0.9. + """ + + def compile_in(self, build_folder, source_folder=None): + raise NotImplementedError + + def copy_build_artifacts( + self, source_folder, build_folder, install_folder, libraries + ): + raise NotImplementedError + + +########################################################################### + + class BuildConfiguration(object): def __init__(self): self.include_dirs = [] @@ -591,7 +614,11 @@ def compile_igraph_from_vendor_source(self): return True vendor_source_path = os.path.join("vendor", "source", "igraph") - if not os.path.isfile(os.path.join(vendor_source_path, "configure.ac")): + if os.path.isfile(os.path.join(vendor_source_path, "configure.ac")): + igraph_builder = IgraphCCoreAutotoolsBuilder() + elif os.path.isfile(os.path.join(vendor_source_path, "CMakeLists.txt")): + igraph_builder = IgraphCCoreCMakeBuilder() + else: # No git submodule present with vendored source print("Cannot find vendored igraph source in " + vendor_source_path) print("") @@ -607,7 +634,6 @@ def compile_igraph_from_vendor_source(self): print(" Install folder: %s" % install_folder) print("") - igraph_builder = IgraphCCoreBuilder() libraries = igraph_builder.compile_in(build_folder, source_folder=source_folder) if not libraries or not igraph_builder.copy_build_artifacts( source_folder=source_folder, From 19d48e47e2accf82f681c7c5db93b0556c7dd652 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 2 Nov 2020 22:14:09 +0100 Subject: [PATCH 0075/1681] CMake builder now builds the C core; work in progress --- setup.py | 257 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 156 insertions(+), 101 deletions(-) diff --git a/setup.py b/setup.py index 5d18b9ba1..ded7fc943 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,20 @@ def exclude_from_list(items, items_to_exclude): return [item for item in items if item not in itemset] +def find_executable(name): + """Finds an executable with the given name on the system PATH and returns + its full path. + """ + try: + from shutil import which + + return which(name) # Python 3.3 and above + except ImportError: + import distutils.spawn + + return distutils.spawn.find_executable(name) # Earlier than Python 3.3 + + def find_static_library(library_name, library_path): """Given the raw name of a library in `library_name`, tries to find a static library with this name in the given `library_path`. `library_path` @@ -261,120 +275,106 @@ class IgraphCCoreAutotoolsBuilder(object): and its friends. This used to be the case before igraph 0.9. """ - def compile_in(self, build_folder, source_folder=None): + def compile_in(self, source_folder, build_folder, install_folder): """Compiles igraph from its source code in the given folder. source_folder is the name of the folder that contains igraph's source - files. If it is `None`, it is assumed that it is the same as the - build folder. - """ - if source_folder is None: - source_folder = build_folder - - source_folder = os.path.abspath(source_folder) - build_folder = os.path.abspath(build_folder) + files. build_folder is the name of the folder where the build should + be executed. Both must be absolute paths. + Returns: + False if the build failed or the list of libraries to link to when + linking the Python interface to igraph + """ build_to_source_folder = os.path.relpath(source_folder, build_folder) - cwd = os.getcwd() - try: - os.chdir(source_folder) - - # Run the bootstrap script if we have downloaded a tarball from - # Github - if os.path.isfile("bootstrap.sh") and not os.path.isfile("configure"): - print("Bootstrapping igraph...") - retcode = subprocess.call("sh bootstrap.sh", shell=True) - if retcode: - return False - - # Patch ltmain.sh so it does not freak out when the build directory - # contains spaces - with open("ltmain.sh") as infp: - with open("ltmain.sh.new", "w") as outfp: - for line in infp: - if line.endswith("cd $darwin_orig_dir\n"): - line = line.replace( - "cd $darwin_orig_dir\n", 'cd "$darwin_orig_dir"\n' - ) - outfp.write(line) - shutil.move("ltmain.sh.new", "ltmain.sh") - - create_dir_unless_exists(build_folder) - os.chdir(build_folder) - - print("Configuring igraph...") - configure_args = ["--disable-tls", "--enable-silent-rules"] - if "IGRAPH_EXTRA_CONFIGURE_ARGS" in os.environ: - configure_args.extend( - os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ") - ) - retcode = subprocess.call( - "sh {0} {1}".format( - quote_path_for_shell( - os.path.join(build_to_source_folder, "configure") - ), - " ".join(configure_args), - ), - env=self.enhanced_env(CFLAGS="-fPIC", CXXFLAGS="-fPIC"), - shell=True, - ) + os.chdir(source_folder) + + # Run the bootstrap script if we have downloaded a tarball from + # Github + if os.path.isfile("bootstrap.sh") and not os.path.isfile("configure"): + print("Bootstrapping igraph...") + retcode = subprocess.call("sh bootstrap.sh", shell=True) if retcode: return False - building_on_windows = building_on_windows_msvc() + # Patch ltmain.sh so it does not freak out when the build directory + # contains spaces + with open("ltmain.sh") as infp: + with open("ltmain.sh.new", "w") as outfp: + for line in infp: + if line.endswith("cd $darwin_orig_dir\n"): + line = line.replace( + "cd $darwin_orig_dir\n", 'cd "$darwin_orig_dir"\n' + ) + outfp.write(line) + shutil.move("ltmain.sh.new", "ltmain.sh") + + os.chdir(build_folder) + + print("Configuring igraph...") + configure_args = ["--disable-tls", "--enable-silent-rules"] + if "IGRAPH_EXTRA_CONFIGURE_ARGS" in os.environ: + configure_args.extend(os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ")) + retcode = subprocess.call( + "sh {0} {1}".format( + quote_path_for_shell(os.path.join(build_to_source_folder, "configure")), + " ".join(configure_args), + ), + env=self.enhanced_env(CFLAGS="-fPIC", CXXFLAGS="-fPIC"), + shell=True, + ) + if retcode: + return False + + building_on_windows = building_on_windows_msvc() - if building_on_windows: - print("Creating Microsoft Visual Studio project...") - retcode = subprocess.call("make msvc", shell=True) - if retcode: - return False + if building_on_windows: + print("Creating Microsoft Visual Studio project...") + retcode = subprocess.call("make msvc", shell=True) + if retcode: + return False - print("Building igraph...") - if building_on_windows: - msvc_source = find_msvc_source_folder() - if not msvc_source: - return False - - devenv = os.environ.get("DEVENV_EXECUTABLE") - os.chdir(msvc_source) - if devenv is None: - retcode = subprocess.call( - "devenv /upgrade igraph.vcproj", shell=True - ) - else: - retcode = subprocess.call([devenv, "/upgrade", "igraph.vcproj"]) - if retcode: - return False + print("Building igraph...") + if building_on_windows: + msvc_source = find_msvc_source_folder() + if not msvc_source: + return False - retcode = subprocess.call( - "msbuild.exe igraph.vcxproj /p:configuration=Release" - ) + devenv = os.environ.get("DEVENV_EXECUTABLE") + os.chdir(msvc_source) + if devenv is None: + retcode = subprocess.call("devenv /upgrade igraph.vcproj", shell=True) else: - retcode = subprocess.call("make", shell=True) - + retcode = subprocess.call([devenv, "/upgrade", "igraph.vcproj"]) if retcode: return False - if building_on_windows: - libraries = ["igraph"] - else: - libraries = [] - for line in open("igraph.pc"): - if line.startswith("Libs: ") or line.startswith("Libs.private: "): - words = line.strip().split() - libraries.extend( - word[2:] for word in words if word.startswith("-l") - ) + retcode = subprocess.call( + "msbuild.exe igraph.vcxproj /p:configuration=Release" + ) + else: + retcode = subprocess.call("make", shell=True) - if not libraries: - # Educated guess - libraries = ["igraph"] + if retcode: + return False - return libraries + if building_on_windows: + libraries = ["igraph"] + else: + libraries = [] + for line in open("igraph.pc"): + if line.startswith("Libs: ") or line.startswith("Libs.private: "): + words = line.strip().split() + libraries.extend( + word[2:] for word in words if word.startswith("-l") + ) - finally: - os.chdir(cwd) + if not libraries: + # Educated guess + libraries = ["igraph"] + + return libraries def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries @@ -431,10 +431,50 @@ class IgraphCCoreCMakeBuilder(object): """Class responsible for downloading and building the C core of igraph if it is not installed yet, assuming that the C core uses CMake as the build tool. This is the case from igraph 0.9. + + Returns: + False if the build failed or the list of libraries to link to when + linking the Python interface to igraph """ - def compile_in(self, build_folder, source_folder=None): - raise NotImplementedError + def compile_in(self, source_folder, build_folder, install_folder): + """Compiles igraph from its source code in the given folder. + + source_folder is the name of the folder that contains igraph's source + files. build_folder is the name of the folder where the build should + be executed. Both must be absolute paths. + """ + cmake = find_executable("cmake") + if not cmake: + print( + "igraph uses CMake as the build system. You need to install CMake " + "before compiling igraph." + ) + return False + + build_to_source_folder = os.path.relpath(source_folder, build_folder) + os.chdir(build_folder) + + print("Configuring build...") + args = [cmake, build_to_source_folder] + for deps in "ARPACK BLAS CXSPARSE GLPK LAPACK".split(): + args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") + + retcode = subprocess.call(args) + if retcode: + return False + + print("Running build...") + retcode = subprocess.call( + [cmake, "--build", ".", "--parallel", "--config", "Release"] + ) + if retcode: + return False + + print("Installing build...") + retcode = subprocess.call([cmake, "--install", ".", "--prefix", install_folder]) + if retcode: + return False def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries @@ -614,10 +654,10 @@ def compile_igraph_from_vendor_source(self): return True vendor_source_path = os.path.join("vendor", "source", "igraph") - if os.path.isfile(os.path.join(vendor_source_path, "configure.ac")): - igraph_builder = IgraphCCoreAutotoolsBuilder() - elif os.path.isfile(os.path.join(vendor_source_path, "CMakeLists.txt")): + if os.path.isfile(os.path.join(vendor_source_path, "CMakeLists.txt")): igraph_builder = IgraphCCoreCMakeBuilder() + elif os.path.isfile(os.path.join(vendor_source_path, "configure.ac")): + igraph_builder = IgraphCCoreAutotoolsBuilder() else: # No git submodule present with vendored source print("Cannot find vendored igraph source in " + vendor_source_path) @@ -634,7 +674,22 @@ def compile_igraph_from_vendor_source(self): print(" Install folder: %s" % install_folder) print("") - libraries = igraph_builder.compile_in(build_folder, source_folder=source_folder) + source_folder = os.path.abspath(source_folder) + build_folder = os.path.abspath(build_folder) + install_folder = os.path.abspath(install_folder) + + create_dir_unless_exists(build_folder) + + cwd = os.getcwd() + try: + libraries = igraph_builder.compile_in( + source_folder=source_folder, + build_folder=build_folder, + install_folder=install_folder, + ) + finally: + os.chdir(cwd) + if not libraries or not igraph_builder.copy_build_artifacts( source_folder=source_folder, build_folder=build_folder, From 1979af57f31b8b4589e105d6e50ac2f9ca383b4f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 5 Nov 2020 08:36:20 +0100 Subject: [PATCH 0076/1681] fix two compiler warnings [ci skip] --- src/_igraph/operators.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c index 7f82f2b35..e748e7a39 100644 --- a/src/_igraph/operators.c +++ b/src/_igraph/operators.c @@ -96,7 +96,7 @@ PyObject *igraphmodule__union(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "graphs", "edgemaps", NULL }; - PyObject *it, *em_list, *graphs, *with_edgemaps_o; + PyObject *it, *em_list = 0, *graphs, *with_edgemaps_o; int with_edgemaps = 0; long int no_of_graphs; igraph_vector_ptr_t gs; @@ -207,7 +207,7 @@ PyObject *igraphmodule__intersection(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "graphs", "edgemaps", NULL }; - PyObject *it, *em_list, *graphs, *with_edgemaps_o; + PyObject *it, *em_list = 0, *graphs, *with_edgemaps_o; int with_edgemaps = 0; long int no_of_graphs; igraph_vector_ptr_t gs; From 6e5f803382b0ff7729dc63556245de12f3dea785 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 5 Nov 2020 19:15:58 +0100 Subject: [PATCH 0077/1681] updated issue templates [skip ci] --- .github/ISSUE_TEMPLATE/bug_report.md | 25 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 4 ++++ .github/ISSUE_TEMPLATE/feature_request.md | 22 ++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..678cf49c2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Report a problem in the Python interface of igraph +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps or minimal example code to reproduce the problem. + +If you are confident that the issue is not in the Python interface but in the +C core of igraph, please add it to the main [igraph repo](https://github.com/igraph/igraph) +instead. + +If you are unsure, feel free to add your issue here - we will transfer it to +the main [igraph repo](https://github.com/igraph/igraph) if the root cause is +in the C core of igraph. + +**Version information** +Which version of `python-igraph` are you using and where did you obtain it? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..60b5562d1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,4 @@ +contact_links: + - name: igraph support + url: https://igraph.discourse.group/ + about: Ask and answer questions about igraph diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..31afa92c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,22 @@ +--- +name: Feature request +about: Suggest an idea for python-igraph +title: '' +labels: '' +assignees: '' + +--- + +**What is the feature or improvement you would like to see?** +A concise description of the requested feature. If the feature request is about +an algorithm, consider adding your request in the main [igraph +repo](https://github.com/igraph/igraph) instead - in most cases the Python +interface simply provides access to the functionality of the igraph library, so +new algorithms should be added there in general. + +**Use cases for the feature** +Explain when and for what purpose the feature would be useful. + +**References** +List any relevant references (papers or books describing relevant algorithms). + From d372c8e986651604d1cd6b2fd56c951dd32228ed Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 3 Dec 2020 23:13:53 +1100 Subject: [PATCH 0078/1681] Bugfix Graph.DataFrame vertex names (#348) --- src/igraph/__init__.py | 80 ++++++++++++++++++++++++++++------------ tests/test_generators.py | 23 +++++++++++- 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 20e9e9cbd..eb200c1ad 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3075,19 +3075,32 @@ def Incidence(klass, *args, **kwds): return result @classmethod - def DataFrame(klass, edges, directed=True, vertices=None): + def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): """DataFrame(edges, directed=True, vertices=None) Generates a graph from one or two dataframes. - @param edges: pandas DataFrame containing edges and metadata + @param edges: pandas DataFrame containing edges and metadata. The first + two columns of this DataFrame contain the source and target vertices + for each edge. These indicate the vertex *names* rather than ids + unless `use_vids` is True and these are nonnegative integers. @param directed: bool setting whether the graph is directed @param vertices: None (default) or pandas DataFrame containing vertex metadata. The first column must contain the unique ids of the - vertices and will be set as attribute 'name'. All other columns - will be added as vertex attributes by column name. + vertices and will be set as attribute 'name'. Although vertex names + are usually strings, they can be any hashable object. All other + columns will be added as vertex attributes by column name. + @use_vids: whether to interpret the first two columns of the `edges` + argument as vertex ids (0-based integers) instead of vertex names. + If this argument is set to True and the first two columns of `edges` + are not integers, an error is thrown. @return: the graph + + Vertex names in either the `edges` or `vertices` arguments that are set + to NaN (not a number) will be set to the string "NA". That might lead + to unexpected behaviour: fill your NaNs with values before calling this + function to mitigate. """ import numpy as np import pandas as pd @@ -3095,11 +3108,21 @@ def DataFrame(klass, edges, directed=True, vertices=None): if edges.shape[1] < 2: raise ValueError("the data frame should contain at least two columns") - # Handle if some elements are 'NA' - if edges.iloc[:, :2].isna().values.any(): - warn("In 'edges' NA elements were replaced with string \"NA\"") - edges = edges.copy() - edges.iloc[:, :2].fillna('NA', inplace=True) + if use_vids: + if str(edges.dtypes[0]).startswith('int') and \ + str(edges.dtypes[1]).startswith('int'): + names_edges = None + else: + raise TypeError('vertex ids must be 0-based integers') + + else: + # Handle if some elements are 'NA' + if edges.iloc[:, :2].isna().values.any(): + warn("In 'edges' NA elements were replaced with string \"NA\"") + edges = edges.copy() + edges.iloc[:, :2].fillna('NA', inplace=True) + + names_edges = np.unique(edges.values[:, :2]) if (vertices is not None) and vertices.iloc[:, 0].isna().values.any(): warn("In the first column of 'vertices' NA elements were replaced "+ @@ -3107,43 +3130,54 @@ def DataFrame(klass, edges, directed=True, vertices=None): vertices = vertices.copy() vertices.iloc[:, 0].fillna('NA', inplace=True) - names_edges = np.unique(edges.values[:, :2]) - if vertices is None: names = names_edges else: if vertices.shape[1] < 1: raise ValueError('vertices has no columns') - names_vertices = vertices.iloc[:, 0].astype(str) + names_vertices = vertices.iloc[:, 0] if names_vertices.duplicated().any(): raise ValueError('Vertex names must be unique') names_vertices = names_vertices.values - if len(np.setdiff1d(names_edges, names_vertices)): + if (names_edges is not None) and \ + len(np.setdiff1d(names_edges, names_vertices)): raise ValueError( - 'Some vertices in the edge DataFrame are missing from vertices DataFrame') + 'Some vertices in the edge DataFrame are missing from '+ + 'vertices DataFrame') names = names_vertices # create graph - g = Graph(n=len(names), directed=directed) + if names is not None: + nv = len(names) + else: + nv = edges.iloc[:, :2].values.max() + 1 + g = Graph(n=nv, directed=directed) + + # vertex names + if names is not None: + for v, name in zip(g.vs, names): + v['name'] = name # vertex attributes - if vertices is not None: + if (vertices is not None) and (vertices.shape[1] > 1): cols = vertices.columns for v, (_, attr) in zip(g.vs, vertices.iterrows()): - v['name'] = attr[cols[0]] - if len(cols) > 1: - for an in cols[1:]: - v[an] = attr[an] + for an in cols[1:]: + v[an] = attr[an] # create edge list - names_idx = pd.Series(index=names, data=np.arange(len(names))) - e0 = names_idx[edges.values[:, 0]] - e1 = names_idx[edges.values[:, 1]] + if names is not None: + names_idx = pd.Series(index=names, data=np.arange(len(names))) + e0 = names_idx[edges.values[:, 0]] + e1 = names_idx[edges.values[:, 1]] + else: + e0 = edges.values[:, 0] + e1 = edges.values[:, 1] # add the edges g.add_edges(zip(e0, e1)) diff --git a/tests/test_generators.py b/tests/test_generators.py index 5dad1405d..e29e3a079 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -177,19 +177,38 @@ def testDataFrame(self): edges = pd.DataFrame( [['C', 'A', 0.4], ['A', 'B', 0.1]], columns=[0, 1, 'weight']) - g = Graph.DataFrame(edges, directed=False) self.assertTrue(g.es["weight"] == [0.4, 0.1]) vertices = pd.DataFrame( [['A', 'blue'], ['B', 'yellow'], ['C', 'blue']], columns=[0, 'color']) - g = Graph.DataFrame(edges, directed=True, vertices=vertices) self.assertTrue(g.vs['name'] == ['A', 'B', 'C']) self.assertTrue(g.vs["color"] == ['blue', 'yellow', 'blue']) self.assertTrue(g.es["weight"] == [0.4, 0.1]) + # Issue #347 + edges = pd.DataFrame({'source': [1, 2, 3], 'target': [4, 5, 6]}) + vertices = pd.DataFrame({ + 'node': [1, 2, 3, 4, 5, 6], + 'label': ['1', '2', '3', '4', '5', '6']})[['node', 'label']] + g = Graph.DataFrame( + edges, + directed=True, + vertices=vertices, + ) + self.assertTrue(g.vs['name'] == [1, 2, 3, 4, 5, 6]) + self.assertTrue(g.vs['label'] == ['1', '2', '3', '4', '5', '6']) + + # Vertex ids + edges = pd.DataFrame({'source': [1, 2, 3], 'target': [4, 5, 6]}) + g = Graph.DataFrame(edges) + self.assertTrue(g.vcount() == 6) + + edges = pd.DataFrame({'source': [1, 2, 3], 'target': [4, 5, 6]}) + g = Graph.DataFrame(edges, use_vids=True) + self.assertTrue(g.vcount() == 7) def suite(): generator_suite = unittest.makeSuite(GeneratorTests) From 6f0f4903c27edd5b25c695f2dc5bd17364d86c97 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 5 Dec 2020 01:40:14 +1100 Subject: [PATCH 0079/1681] Prototype for matplotlib graph drawing (#341) --- src/igraph/drawing/__init__.py | 16 ++- src/igraph/drawing/edge.py | 3 - src/igraph/drawing/graph.py | 248 ++++++++++++++++++++++++++++++++- src/igraph/drawing/utils.py | 25 +++- 4 files changed, 279 insertions(+), 13 deletions(-) diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index d39109786..83b38c4e9 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -27,8 +27,8 @@ from igraph.compat import property, BytesIO from igraph.configuration import Configuration from igraph.drawing.colors import Palette, palettes -from igraph.drawing.graph import DefaultGraphDrawer -from igraph.drawing.utils import BoundingBox, Point, Rectangle, find_cairo +from igraph.drawing.graph import DefaultGraphDrawer, MatplotlibGraphDrawer +from igraph.drawing.utils import BoundingBox, Point, Rectangle, find_cairo, find_matplotlib from igraph.utils import _is_running_in_ipython, named_temporary_file __all__ = ["BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot"] @@ -36,6 +36,7 @@ __license__ = "GPL" cairo = find_cairo() +matplotlib, plt = find_matplotlib() ##################################################################### @@ -50,7 +51,7 @@ class Plot(object): since C{pycairo} takes care of the actual drawing. Everything that's supported by C{pycairo} should be supported by this class as well. - Current Cairo surfaces that I'm aware of are: + Current Cairo surfaces include: - C{cairo.GlitzSurface} -- OpenGL accelerated surface for the X11 Window System. @@ -385,8 +386,10 @@ def width(self): is drawn""" return self.bbox.width + ##################################################################### + def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): """Plots the given object to the given target. @@ -449,6 +452,11 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @see: Graph.__plot__ """ + if hasattr(plt, 'Axes') and isinstance(target, plt.Axes): + result = MatplotlibGraphDrawer(ax=target) + result.draw(obj, *args, **kwds) + return + if not isinstance(bbox, BoundingBox): bbox = BoundingBox(bbox) @@ -482,7 +490,7 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): # so just show or save the result if target is None: result.show() - elif isinstance(target, basestring): + elif isinstance(target, str): result.save() # Also return the plot itself diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 077e1fc15..64e5ffc22 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -234,7 +234,6 @@ def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): return bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) - # Draw the edge ctx.set_source_rgba(*edge.color) ctx.set_line_width(edge.width) @@ -313,7 +312,6 @@ def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): # Draw the edge ctx.stroke() - # Draw the arrow head ctx.move_to(x2, y2) ctx.line_to(*aux_points[0]) @@ -322,7 +320,6 @@ def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): ctx.fill() - class TaperedEdgeDrawer(AbstractEdgeDrawer): """Edge drawer implementation that draws undirected edges as straight lines and directed edges as tapered lines that are diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index c24f09ffd..0491c851e 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -15,7 +15,7 @@ from collections import defaultdict from itertools import izip -from math import atan2, cos, pi, sin, tan +from math import atan2, cos, pi, sin, tan, sqrt from warnings import warn from igraph._igraph import convex_hull, VertexSeq @@ -576,7 +576,7 @@ class VisualEdgeBuilder(AttributeCollectorBase): # Add the vertices n = graph.vcount() new_vertex = display.new_vertex - vertex_ids = [new_vertex() for _ in xrange(n)] + vertex_ids = [new_vertex() for _ in range(n)] # Add the edges new_edge = display.new_edge @@ -674,10 +674,10 @@ def draw(self, graph, name="Network from igraph", create_view=True, # Create the nodes if "node_ids" in kwds: node_ids = kwds["node_ids"] - if isinstance(node_ids, basestring): + if isinstance(node_ids, str): node_ids = graph.vs[node_ids] else: - node_ids = xrange(graph.vcount()) + node_ids = range(graph.vcount()) node_ids = [str(identifier) for identifier in node_ids] cy.createNodes(network_id, node_ids) @@ -926,3 +926,243 @@ def draw(self, graph, *args, **kwds): will be used to encode the JSON objects. """ self.streamer.post(graph, self.connection, encoder=kwds.get("encoder")) + +##################################################################### + +class MatplotlibGraphDrawer(AbstractGraphDrawer): + """Graph drawer that uses a pyplot.Axes as context""" + + _shape_dict = { + 'rectangle': 's', + 'circle': 'o', + 'hidden': 'none', + 'triangle-up': '^', + 'triangle-down': 'v', + } + + + + def __init__(self, ax): + """Constructs the graph drawer and associates it with the mpl axes""" + self.ax = ax + + def draw(self, graph, *args, **kwds): + import matplotlib.markers as mmarkers + from matplotlib.path import Path + from matplotlib.patches import FancyArrowPatch + from matplotlib.patches import ArrowStyle + + def shrink_vertex(ax, aux, vcoord, vsize_squared): + '''Shrink edge by vertex size''' + aux_display, vcoord_display = ax.transData.transform([aux, vcoord]) + d = sqrt(((aux_display - vcoord_display)**2).sum()) + fr = sqrt(vsize_squared) / d + end_display = vcoord_display + fr * (aux_display - vcoord_display) + end = ax.transData.inverted().transform(end_display) + return end + + def callback_factory(ax, vcoord, vsizes, arrows): + def callback_edge_offset(event): + for arrow, src, tgt in arrows: + v1, v2 = vcoord[src], vcoord[tgt] + # This covers both cases (curved and straight) + aux1, aux2 = arrow._path_original.vertices[[1, -2]] + start = shrink_vertex(ax, aux1, v1, vsizes[src]) + end = shrink_vertex(ax, aux2, v2, vsizes[tgt]) + arrow._path_original.vertices[0] = start + arrow._path_original.vertices[-1] = end + + return callback_edge_offset + + ax = self.ax + + # FIXME: deal with unnamed *args + + # Get layout + layout = kwds.get('layout', graph.layout()) + if isinstance(layout, str): + layout = graph.layout(layout) + + # Vertex coordinates + vcoord = layout.coords + + # Vertex properties + vsizes = kwds.get('vertex_size', None) + # NOTE: ax.scatter uses the *square* of diameter + if vsizes is not None: + vsizes **= 2 + if isinstance(vsizes, float) or isinstance(vsizes, int): + vsizes = [vsizes] * len(vcoord) + c = kwds.get('vertex_color', 'black') + alpha = kwds.get('alpha', 1.0) + label = kwds.get('vertex_label', None) + label_size = kwds.get('vertex_label_size', None) + vzorder = kwds.get('vertex_order', 2) + # mpl shapes use slightly different names from Cairo + shapes = kwds.get('vertex_shape', None) + if shapes is not None: + if isinstance(shapes, str): + shapes = self._shape_dict.get(shapes, shapes) + elif isinstance(shapes, mmarkers.MarkerStyle): + pass + + # Scatter vertices + # NOTE: matplotlib does not support a list of shapes yet + x, y = list(zip(*vcoord)) + ax.scatter(x, y, s=vsizes, c=c, marker=shapes, zorder=vzorder, alpha=alpha) + + # Vertex labels + if label is not None: + for i, lab in enumerate(label): + xi, yi = x[i], y[i] + ax.text(xi, yi, lab, fontsize=label_size) + + dx = (max(x) - min(x)) + dy = (max(y) - min(y)) + ax.set_xlim(min(x) - 0.05 * dx, max(x) + 0.05 * dx) + ax.set_ylim(min(y) - 0.05 * dy, max(y) + 0.05 * dy) + + # Edge properties + ne = graph.ecount() + ec = kwds.get('edge_color', 'black') + edge_width = kwds.get('edge_width', 1) + arrow_width = kwds.get('edge_arrow_width', 2) + arrow_length = kwds.get('edge_arrow_size', 4) + ealpha = kwds.get('edge_alpha', 1.0) + ezorder = kwds.get('edge_order', 1.0) + try: + ezorder = float(ezorder) + ezorder = [ezorder] * ne + except TypeError: + pass + + # Decide whether we need to calculate the curvature of edges + # automatically -- and calculate them if needed. + autocurve = kwds.get("autocurve", None) + if autocurve or (autocurve is None and \ + "edge_curved" not in kwds and "curved" not in graph.edge_attributes() \ + and graph.ecount() < 10000): + from igraph import autocurve + default = kwds.get("edge_curved", 0) + if default is True: + default = 0.5 + default = float(default) + kwds["edge_curved"] = autocurve(graph, attribute=None, default=default) + + # Arrow style for directed and undirected graphs + if graph.is_directed(): + arrowstyle = ArrowStyle( + '-|>', head_length=arrow_length, head_width=arrow_width, + ) + else: + arrowstyle = '-' + + # Edge coordinates and curvature + nloops = [0 for x in range(ne)] + has_curved = 'curved' in graph.es.attributes() + arrows = [] + for ie, edge in enumerate(graph.es): + src, tgt = edge.source, edge.target + x1, y1 = vcoord[src] + x2, y2 = vcoord[tgt] + + # Loops require special treatment + if src == tgt: + # Find all non-loop edges + nloopstot = 0 + angles = [] + for tgtn in graph.neighbors(src): + if tgtn == src: + nloopstot += 1 + continue + xn, yn = vcoord[tgtn] + angles.append(180. / pi * atan2(yn - y1, xn - x1) % 360) + # with .neighbors(mode=ALL), which is default, loops are double + # counted + nloopstot //= 2 + angles = sorted(set(angles)) + + # Only loops or one non-loop + if len(angles) < 2: + ashift = angles[0] if angles else 270 + if nloopstot == 1: + # Only one self loop, use a quadrant only + angles = [(ashift + 135) % 360, (ashift + 225) % 360] + else: + nshift = 360. / nloopstot + angles = [(ashift + nshift * nloops[src]) % 360, + (ashift + nshift * (nloops[src] + 1)) % 360] + nloops[src] += 1 + else: + angles.append(angles[0] + 360) + idiff = 0 + diff = 0 + for i in range(len(angles) - 1): + diffi = abs(angles[i + 1] - angles[i]) + if diffi > diff: + idiff = i + diff = diffi + angles = angles[idiff: idiff + 2] + ashift = angles[0] + nshift = (angles[1] - angles[0]) / nloopstot + angles = [(ashift + nshift * nloops[src]), + (ashift + nshift * (nloops[src] + 1))] + nloops[src] += 1 + + # this is not great, but alright + angspan = angles[1] - angles[0] + if angspan < 180: + angmid1 = angles[0] + 0.1 * angspan + angmid2 = angles[1] - 0.1 * angspan + else: + angmid1 = angles[0] + 0.5 * (angspan - 180) + 45 + angmid2 = angles[1] - 0.5 * (angspan - 180) - 45 + aux1 = (x1 + 0.2 * dx * cos(pi / 180 * angmid1), + y1 + 0.2 * dy * sin(pi / 180 * angmid1)) + aux2 = (x1 + 0.2 * dx * cos(pi / 180 * angmid2), + y1 + 0.2 * dy * sin(pi / 180 * angmid2)) + start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) + end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) + + path = Path( + [start, aux1, aux2, end], + # Cubic bezier by mpl + codes=[1, 4, 4, 4]) + + else: + curved = edge['curved'] if has_curved else False + if curved: + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), \ + (2 * y1 + y2) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), \ + (y1 + 2 * y2) / 3.0 + edge.curved * 0.5 * (x2 - x1) + start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) + end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) + + path = Path( + [start, aux1, aux2, end], + # Cubic bezier by mpl + codes=[1, 4, 4, 4]) + else: + start = shrink_vertex(ax, (x2, y2), (x1, y1), vsizes[src]) + end = shrink_vertex(ax, (x1, y1), (x2, y2), vsizes[tgt]) + + path = Path([start, end], codes=[1, 2]) + + arrow = FancyArrowPatch( + path=path, + arrowstyle=arrowstyle, + lw=edge_width, + color=ec, + alpha=ealpha, + zorder=ezorder[ie]) + ax.add_artist(arrow) + + # Store arrows and their sources and targets for autoscaling + arrows.append((arrow, src, tgt)) + + # Autoscaling during zoom, figure resize, reset axis limits + callback = callback_factory(ax, vcoord, vsizes, arrows) + ax.get_figure().canvas.mpl_connect('resize_event', callback) + ax.callbacks.connect('xlim_changed', callback) + ax.callbacks.connect('ylim_changed', callback) diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 47f601687..205b55044 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -393,11 +393,11 @@ class FakeModule(object): """Fake module that raises an exception for everything""" def __getattr__(self, _): - raise TypeError("plotting not available") + raise AttributeError("plotting not available") def __call__(self, _): raise TypeError("plotting not available") def __setattr__(self, key, value): - raise TypeError("plotting not available") + raise AttributeError("plotting not available") ##################################################################### @@ -418,6 +418,27 @@ def find_cairo(): ##################################################################### +def find_matplotlib(): + """Tries to import the ``cairo`` Python module if it is installed, + also trying ``cairocffi`` (a drop-in replacement of ``cairo``). + Returns a fake module if everything fails. + """ + try: + import matplotlib as mpl + has_mpl = True + except ImportError: + mpl = FakeModule() + has_mpl = False + + if has_mpl: + import matplotlib.pyplot as plt + else: + plt = FakeModule() + + return mpl, plt + +##################################################################### + class Point(tuple): """Class representing a point on the 2D plane.""" __slots__ = () From 06f8bb9705234e7a0053c6057bb522302e897276 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 5 Dec 2020 01:41:16 +1100 Subject: [PATCH 0080/1681] Documentation on graph visualization (#340) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tamás Nepusz --- doc/source/visualisation.rst | 189 ++++++++++++++++++++++++++++++++++- 1 file changed, 188 insertions(+), 1 deletion(-) diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index 53b9d0bcb..7f12491d2 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -1,4 +1,191 @@ +.. include:: include/global.rst + +======================= Visualisation of graphs ======================= +|igraph| includes functionality to visualize graphs. There are two main components: graph layouts and graph plotting. + +In the following examples, we will assume |igraph| is imported as `ig` and a +:class:`Graph` object has been previously created, e.g.: + +>>> import igraph as ig +>>> g = ig.Graph(edges=[[0, 1], [2, 3]]) + +Read the `API documentation`_ for details on each function and class. The `tutorial`_ contains examples to get started. + +Graph layouts +============= +A graph *layout* is a low-dimensional (usually: 2 dimensional) representation of a graph. Different layouts for the same +graph can be computed and typically preserve or highlight distinct properties of the graph itself. Some layouts only make +sense for specific kinds of graphs, such as trees. + +|igraph| offers several graph layouts. The general function to compute a graph layout is :meth:`Graph.layout`: + +>>> layout = g.layout(layout='auto') + +See below for a list of supported layouts. The resulting object is an instance of `igraph.layout.Layout` and has some +useful properties: + +- :attr:`Layout.coords`: the coordinates of the vertices in the layout (each row is a vertex) +- :attr:`Layout.dim`: the number of dimensions of the embedding (usually 2) + +and methods: + +- :meth:`Layout.boundaries` the rectangle with the extreme coordinates of the layout +- :meth:`Layout.bounding_box` the boundary, but as an `igraph.drawing.utils.BoundingBox` (see below) +- :meth:`Layout.centroid` the coordinates of the centroid of the graph layout + +Indexing and slicing can be performed and returns the coordinates of the requested vertices: + +>>> coords_subgraph = layout[:2] # Coordinates of the first two vertices + +.. note:: The returned object is a list of lists with the coordinates, not an `igraph.layout.Layout` + object. You can wrap the result into such an object easily: + + >>> layout_subgraph = ig.Layout(coords=layout[:2]) + +It is possible to perform linear transformations to the layout: + +- :meth:`Layout.translate` +- :meth:`Layout.center` +- :meth:`Layout.scale` +- :meth:`Layout.fit_into` +- :meth:`Layout.rotate` +- :meth:`Layout.mirror` + +as well as a generic nonlinear transformation via: + +- :meth:`Layout.transform` + +The following regular layouts are supported: + +- `Graph.layout_star`: star layout +- `Graph.layout_circle`: circular/spherical layout +- `Graph.layout_grid`: regular grid layout in 2D +- `Graph.layout_grid_3d`: regular grid layout in 3D +- `Graph.layout_random`: random layout (2D and 3D) + +The following algorithms produce nice layouts for general graphs: + +- `Graph.layout_davidson_harel`: Davidson-Harel layout, based on simulated annealing optimization including edge crossings +- `Graph.layout_drl`: DrL layout for large graphs (2D and 3D), a scalable force-directed layout +- `Graph.layout_fruchterman_reingold`: Fruchterman-Reingold layout (2D and 3D), a "spring-electric" layout based on classical physics +- `Graph.layout_graphopt`: the graphopt algorithm, another force-directed layout +- `Graph.layout_kamada_kawai`: Kamada-Kawai layout (2D and 3D), a "spring" layout based on classical physics +- `Graph.layout_lgl`: Large Graph Layout +- `Graph.layout_mds`: multidimensional scaling layout + +The following algorithms are useful for *trees* (and for Sugiyama *directed acyclic graphs* or *DAGs*): + +- `Graph.layout_reingold_tilford`: Reingold-Tilford layout +- `Graph.layout_reingold_tilford_circular`: circular Reingold-Tilford layout +- `Graph.layout_sugiyama`: Sugiyama layout, a hierarchical layout + +For *bipartite graphs*, there is a dedicated function: + +- `Graph.layout_bipartite`: bipartite layout + +More might be added in the future, based on request. + +Graph plotting +============== +Once the layout of a graph has been computed, |igraph| can assist with the plotting itself. Plotting happens within a single +function, `igraph.plot`. + +Plotting with the default image viewer +++++++++++++++++++++++++++++++++++++++ +A naked call to `igraph.plot` generates a temporary file and opens it with the default image viewer: + +>>> ig.plot(g) + +(see below if you are using this in a `Jupyter`_ notebook). This uses the `Cairo`_ library behind the scenes. + +Saving a plot to a file ++++++++++++++++++++++++ +A call to `igraph.plot` with a `target` argument stores the graph image in the specified file and does *not* +open it automatically. Based on the filename extension, any of the following output formats can be chosen: +PNG, PDF, SVG and PostScript: + +>>> ig.plot(g, target='myfile.pdf') + +.. note:: PNG is a raster image format while PDF, SVG, and Postscript are vector image formats. Choose one of the last three + formats if you are planning on refining the image with a vector image editor such as Inkscape or Illustrator. + +Plotting graphs within Matplotlib figures ++++++++++++++++++++++++++++++++++++++++++ +If the target argument is a `matplotlib`_ axes, the graph will be plotted inside that axes: + +>>> import matplotlib.pyplot as plt +>>> fig, ax = plt.subplots() +>>> ig.plot(g, target=ax) + +You can then further manipulate the axes and figure however you like via the `ax` and `fig` variables (or whatever you +called them). This variant does not use `Cairo`_ directly and might be lacking some features that are available in the +`Cairo`_ backend: please open an issue on Github to request specific features. + +Plotting graphs in Jupyter notebooks +++++++++++++++++++++++++++++++++++++ +|igraph| supports inline plots within a `Jupyter`_ notebook via both the `Cairo`_ and `matplotlib`_ backend. If you are +calling `igraph.plot` from a notebook cell without a `matplotlib`_ axes, the image will be shown inline in the corresponding +output cell. Use the `bbox` argument to scale the image while preserving the size of the vertices, text, and other artists. +For instance, to get a compact plot: + +>>> ig.plot(g, bbox=(0, 0, 100, 100)) + +These inline plots can be either in PNG or SVG format. There is currently an open bug that makes SVG fail if more than one graph +per notebook is plotted: we are working on a fix for that. In the meanwhile, you can use PNG representation. + +If you want to use the `matplotlib`_ engine in a Jupyter notebook, you can use the recipe above. First create an axes, then +tell `igraph.plot` about it via the `target` argument: + +>>> import matplotlib.pyplot as plt +>>> fig, ax = plt.subplots() +>>> ig.plot(g, target=ax) + +Exporting to other graph formats +++++++++++++++++++++++++++++++++++ +If igraph is missing a certain plotting feature and you cannot wait for us to include it, you can always export your graph +to a number of formats and use an external graph plotting tool. We support both conversion to file (e.g. DOT format used by +`graphviz`_) and to popular graph libraries such as `networkx`_ and `graph-tool`_: + +>>> dot = g.write('/myfolder/myfile.dot') +>>> n = g.to_networkx() +>>> gt = g.to_graph_tool() + +You do not need to have any libraries installed if you export to file, but you do need them to convert directly to external +Python objects (`networkx`_, `graph-tool`_). + +Plotting options +==================== +You can add an argument `layout` to the `plot` function to specify a precomputed layout, e.g.: + +>>> layout = g.layout("kamada_kawai") +>>> ig.plot(g, layout=layout) + +It is also possible to use the name of the layout algorithm directly: + +>>> ig.plot(g, layout="kamada_kawai") + +If the layout is left unspecified, igraph uses the dedicated `layout_auto()` function, which chooses between one of several +possible layouts based on the number of vertices and edges. + +You can also specify vertex and edge color, size, and labels - and more - via additional arguments, e.g.: + +>>> ig.plot(g, +>>> vertex_size=20, +>>> vertex_color=['blue', 'red', 'green', 'yellow'], +>>> vertex_label=['first', 'second', 'third', 'fourth'], +>>> edge_width=[1, 4], +>>> edge_color=['black', 'grey'], +>>> ) + +See the `tutorial`_ for examples and a full list of options. -.. note:: TODO. This is a placeholder section; it is not written yet. +.. _API documentation: https://igraph.org/python/doc/igraph-module.html +.. _matplotlib: https://matplotlib.org +.. _Jupyter: https://jupyter.org/ +.. _tutorial: https://igraph.org/python/doc/tutorial/tutorial.html#layouts-and-plotting +.. _Cairo: https://www.cairographics.org +.. _graphviz: http://www.graphviz.org +.. _networkx: https://networkx.org/ +.. _graph-tool: https://graph-tool.skewed.de/ From 90e56251af00b30540fd06951a7423de249b8fd0 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sun, 6 Dec 2020 17:40:42 +1100 Subject: [PATCH 0081/1681] Bugfix #349 for in recent matplotlib drawing PR --- src/igraph/drawing/graph.py | 43 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 0491c851e..f713d2b67 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -947,10 +947,13 @@ def __init__(self, ax): self.ax = ax def draw(self, graph, *args, **kwds): + # NOTE: matplotlib has numpy as a dependency, so we can use it in here + import matplotlib as mpl import matplotlib.markers as mmarkers from matplotlib.path import Path from matplotlib.patches import FancyArrowPatch from matplotlib.patches import ArrowStyle + import numpy as np def shrink_vertex(ax, aux, vcoord, vsize_squared): '''Shrink edge by vertex size''' @@ -987,19 +990,38 @@ def callback_edge_offset(event): vcoord = layout.coords # Vertex properties - vsizes = kwds.get('vertex_size', None) - # NOTE: ax.scatter uses the *square* of diameter - if vsizes is not None: - vsizes **= 2 - if isinstance(vsizes, float) or isinstance(vsizes, int): - vsizes = [vsizes] * len(vcoord) - c = kwds.get('vertex_color', 'black') + nv = graph.vcount() + + # Vertex size + vsizes = kwds.get('vertex_size', 5) + # Enforce numpy array for sizes, because (1) we need the square and (2) + # they are needed to calculate autoshrinking of edges + if np.isscalar(vsizes): + vsizes = np.repeat(vsizes, nv) + else: + vsizes = np.asarray(vsizes) + # ax.scatter uses the *square* of diameter + vsizes **= 2 + + # Vertex color + c = kwds.get('vertex_color', 'steelblue') + + # Vertex opacity alpha = kwds.get('alpha', 1.0) + + # Vertex labels label = kwds.get('vertex_label', None) - label_size = kwds.get('vertex_label_size', None) + + # Vertex label size + label_size = kwds.get('vertex_label_size', mpl.rcParams['font.size']) + + # Vertex zorder vzorder = kwds.get('vertex_order', 2) - # mpl shapes use slightly different names from Cairo - shapes = kwds.get('vertex_shape', None) + + # Vertex shapes + # mpl shapes use slightly different names from Cairo, but we want the + # API to feel consistent, so we use a conversion dictionary + shapes = kwds.get('vertex_shape', 'o') if shapes is not None: if isinstance(shapes, str): shapes = self._shape_dict.get(shapes, shapes) @@ -1007,7 +1029,6 @@ def callback_edge_offset(event): pass # Scatter vertices - # NOTE: matplotlib does not support a list of shapes yet x, y = list(zip(*vcoord)) ax.scatter(x, y, s=vsizes, c=c, marker=shapes, zorder=vzorder, alpha=alpha) From 12e1654fb6701941e61889a63f6c54b78aa41aab Mon Sep 17 00:00:00 2001 From: MapleCCC Date: Sun, 6 Dec 2020 20:27:46 +0800 Subject: [PATCH 0082/1681] Fix outdated link and typo (#352) * Fix typo in tutorial * Fix outdated link to "A Byte of Python" in tutorial --- doc/source/tutorial.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 76acd1919..16ac2c6be 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -11,7 +11,7 @@ it at least once if you are new to |igraph|. I assume that you have already inst you did not, see :ref:`installing-igraph` first. Familiarity with the Python language is also assumed; if this is the first time you are trying to use Python, there are many good Python tutorials on the Internet to get you started. If this is the first time you ever try to use a -programming language, `A Byte of Python `_ is a good place to +programming language, `A Byte of Python `_ is a good place to start out. If you already have a stable programming background in other languages and you just want a quick overview of Python, `Learn Python in 10 minutes `_ is probably your best bet. @@ -40,7 +40,7 @@ following: Another way to make use of |igraph| is to import all its objects and methods into the main Python namespace (so you do not have to type the namespace-qualification every time). -This is fine as long as none of your own objects and methods do not conflict with the ones +This is fine as long as your own objects and methods do not conflict with the ones provided by |igraph|: >>> from igraph import * @@ -665,7 +665,7 @@ the screen or to a PDF, PNG or SVG file using the `Cairo library Date: Fri, 11 Dec 2020 18:37:36 +1030 Subject: [PATCH 0083/1681] Install from source instructions (#351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tamás Nepusz --- doc/source/install.rst | 121 ++++++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 19 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 6f067f093..b5aeb1599 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -40,6 +40,11 @@ and macOS. Currently there are binary packages for Python 2.7, and Python 3.5 th note that support for Python 2.7 will be discontinued with the version 0.9.0 release of |igraph|'s Python interface. +To install |python-igraph| globally, use the following command (you probably need +administrator/root priviledges): + + $ pip install python-igraph + Many users like to install packages into a project-specific `virtual environment `_. A variation of the following commands should work on most platforms: @@ -48,14 +53,20 @@ A variation of the following commands should work on most platforms: $ source venv/bin/activate $ pip install python-igraph -To test the installed package, launch Python within the virtual environment and run the -following: +Alternatively, if you do not want to activate the virtualenv, you can call the `pip` executable +in it directly: - >>> import igraph.test - >>> igraph.test.run_tests() + $ python -m venv venv + $ venv/bin/pip install python-igraph -The above commands run the bundled test cases to ensure that everything is fine with your -|igraph| installation. +The unit tests of `python-igraph` are implemented with the standard `unittest` module so you can +run them like this from your virtualenv: + + $ python -m unittest discover + +As usual, you can also do this without activating the virtualenv: + + $ venv/bin/python -m unittest discover Installing |igraph| via Conda ----------------------------- @@ -77,7 +88,7 @@ The above commands run the bundled test cases to ensure that everything is fine |igraph| on Windows ------------------- -There is a Windows installer for |igraph|'s Python interface on the `Python Package Index +Precompiled Windows wheels for |igraph|'s Python interface are available on the `Python Package Index `_ (see `Installing igraph from the Python Package Index`_). @@ -104,11 +115,9 @@ If PyCairo was successfully installed, this will display a Petersen graph. |igraph| on macOS ----------------- -The Mac installer for |igraph|'s Python interface on the `Python Package Index -`_ works for Intel-based Macs only. PowerPC -users should compile the package themselves (see `Compiling igraph from source`_). - -TODO: Check if PyCairo on Mac still requires using Brew +Precompiled macOS wheels for |igraph|'s Python interface are available on the `Python Package Index +`_ (see `Installing igraph from the Python Package +Index`_). Graph plotting in |igraph| is implemented using a third-party package called `Cairo `_. If you want to create publication-quality plots in |igraph| @@ -131,13 +140,87 @@ If Cairo was successfully installed, this will display a Petersen graph. ----------------- |igraph|'s Python interface and its dependencies have been packaged for most popular Linux -distributions, including Arch Linux, Debian, Fedora, GNU Guix, NixOS, and Ubuntu. Because -distribution packages are often outdated, you may choose to install |igraph| from the `Python -Package Index `_ instead to get a more recent -release (see `Installing igraph from the Python Package Index`_). +distributions, including Arch Linux, Debian, Fedora, GNU Guix, NixOS, and Ubuntu. +|python-igraph| and its underlying |igraph| C core are usually in two different packages, but +your package manager should take care of that dependency for you. + +.. note:: Distribution packages can be outdated: if you find that's the case for you, you may + choose to install |igraph| from the `Python Package Index `_ + instead to get a more recent release (see `Installing igraph from the Python Package Index`_). + +Compiling |python-igraph| from source +===================================== + +|python-igraph| binds itself into the main |igraph| library using some glue code written in C, so you'll need +both a C compiler and the library itself. Source tarballs of |python-igraph| obtained from PyPI already +contain a matching version of the C core of |igraph|. + +There are two common scenarios to compile |python-igraph| from source: + +1. Your would like to use the latest development version from Github, usually to try out some recently + added features + +2. The PyPI repository does not include precompiled binaries for your system. This can happen if your operating + system is not covered by our continuous development. + +Both will be covered in the next sections. + +Compiling using pip +------------------- +If you want the development version of |python-igraph|, call: + + $ pip install git+https://github.com/igraph/python-igraph + +`pip` is smart enough to download the sources from Github, initialize the submodule for the |igraph| C core, +compile it, and then compile |python-igraph| against it and install it. As above, a virtual environment is +a commonly used sandbox to test experimental packages. + +If you want the latest release from PyPI but prefer to (or have to) install from source, call: + + $ pip install --no-binary ':all:' python-igraph + +.. note:: If there is no binary for your system anyway, you can just try without the `--no-binary` option and + obtain the same result. + +Compiling step by step +---------------------- +This section should be rarely used in practice but explains how to compile and install |python-igraph| step +by step without `pip`. + +First, obtain the bleeding-edge source code from Github: + + $ git clone https://github.com/igraph/python-igraph.git + +or download a recent release from `PyPI ` or from the +`Github releases page `. Decompress the archive if +needed. + +Second, go into the folder: + + $ cd python-igraph + +(it might have a slightly different name depending on the release). + +Third, if you cloned the source from Github, initialize the `git` submodule for the |igraph| C core: + + $ git submodule update --init + +.. note:: If you prefer to compile and link |python-igraph| against an existing |igraph| C core, for instance + the one you installed with your package manager, you can skip the `git` submodule initialization step. If you + downloaded a tarball, you also need to remove the `vendor/source/igraph` folder because the setup script + will look for the vendored |igraph| copy first. However, a particular version of |python-igraph| is guaranteed + to work only with the version of the C core that is bundled with it (or with the revision that the `git` + submodule points to). + +Fourth, call the standard Python `setup.py` script, e.g. for compiling: + + $ python setup.py build +(press Enter when prompted). That will compile the |python-igraph| package in a subfolder called +`build/lib.`, e.g. `build/lib.linux-x86_64-3.8`. You can add +that folder to your `PYTHONPATH` if you want to import directly from it, or you can call the `setup.py` +script to install it from there: -Compiling |igraph| from source -============================== + $ python setup.py install -TODO +.. note:: The `setup.py` script takes a number of options to customize the install location. From 1ef2215c4c40183eb810b28ba08cdc5690655d67 Mon Sep 17 00:00:00 2001 From: Thierry Thomas Date: Sat, 2 Jan 2021 10:30:26 +0000 Subject: [PATCH 0084/1681] Adding an image viewer for FreeBSD (#354) --- src/igraph/configuration.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index df33886f3..4a50ce4ea 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -56,6 +56,18 @@ def get_platform_image_viewer(): if os.path.isfile(full_path): return full_path return "" + elif plat == "FreeBSD": + # FreeBSD also has a whole lot of choices, try to find one + choices = ["eog", "gthumb", "geeqie", "display", + "gpicview", "gwenview", "qiv", "gimv", "ristretto", + "geeqie", "eom"] + paths = ["%%LOCALBASE%%/bin"] + for path in paths: + for choice in choices: + full_path = os.path.join(path, choice) + if os.path.isfile(full_path): + return full_path + return "" elif plat == "Windows" or plat == "Microsoft": # Thanks to Dale Hunscher # Use the built-in Windows image viewer, if available return "start" From 0a3b81e14660d2a79f5695ed0ac056a18396de8d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 Jan 2021 12:54:35 +0100 Subject: [PATCH 0085/1681] ci: try Appveyor without zstd --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a1cfd8375..782978be6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,6 @@ install: # update msys2 - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors" - bash -lc "pacman --noconfirm -Sy" - - bash -lc "pacman --needed --noconfirm -S zstd" - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" # prepare Python From d206314ed20819ff1a6396f0e3896b517b4f8361 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 Jan 2021 12:57:41 +0100 Subject: [PATCH 0086/1681] ci: try a newer Appveyor image --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 782978be6..69a63209e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,5 @@ image: - - Visual Studio 2017 + - Visual Studio 2019 environment: global: From ebcc444c10c22db627b21b809ef15a4888d5dc94 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 Jan 2021 13:37:00 +0100 Subject: [PATCH 0087/1681] ci: adjust VS path in appveyor.yml for new image --- appveyor.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 69a63209e..4e3ee3605 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,11 +25,6 @@ platform: - x64 install: - # update msys2 keyring first - - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" - - bash -lc "curl -O http://repo.msys2.org/msys/x86_64/msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz.sig" - - bash -lc "pacman --noconfirm -U msys2-keyring-r21.b39fb11-1-any.pkg.tar.xz" - # update msys2 - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors" - bash -lc "pacman --noconfirm -Sy" @@ -51,13 +46,13 @@ for: only: - MSYSTEM: MINGW32 build_script: - - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars32.bat" + - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat" - cibuildwheel --output-dir wheelhouse - matrix: only: - MSYSTEM: MINGW64 build_script: - - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvars64.bat" + - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" - cibuildwheel --output-dir wheelhouse artifacts: From d353fa03f456dde825fd89eea8a1d8c01f18cb8e Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 15 Jan 2021 21:03:24 +1100 Subject: [PATCH 0088/1681] Abandon Python 2 and lint black --- setup.py | 11 +- src/igraph/__init__.py | 1169 +++++---- src/igraph/app/shell.py | 144 +- src/igraph/clustering.py | 246 +- src/igraph/compat.py | 75 - src/igraph/configuration.py | 98 +- src/igraph/cut.py | 52 +- src/igraph/datatypes.py | 186 +- src/igraph/drawing/__init__.py | 73 +- src/igraph/drawing/baseclasses.py | 28 +- src/igraph/drawing/colors.py | 3902 +++++++++++------------------ src/igraph/drawing/coord.py | 15 +- src/igraph/drawing/edge.py | 269 +- src/igraph/drawing/graph.py | 392 +-- src/igraph/drawing/metamagic.py | 65 +- src/igraph/drawing/shapes.py | 136 +- src/igraph/drawing/text.py | 40 +- src/igraph/drawing/utils.py | 127 +- src/igraph/drawing/vertex.py | 32 +- src/igraph/formula.py | 40 +- src/igraph/layout.py | 124 +- src/igraph/matching.py | 25 +- src/igraph/operators.py | 75 +- src/igraph/remote/gephi.py | 23 +- src/igraph/statistics.py | 199 +- src/igraph/summary.py | 98 +- src/igraph/test/foreign.py | 288 ++- src/igraph/utils.py | 56 +- tests/test_structural.py | 2 +- tox.ini | 5 +- 30 files changed, 3796 insertions(+), 4199 deletions(-) delete mode 100644 src/igraph/compat.py diff --git a/setup.py b/setup.py index ded7fc943..f92ddda81 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ ########################################################################### # Check Python's version info and exit early if it is too old -if sys.version_info < (2, 7): - print("This module requires Python >= 2.7") +if sys.version_info < (3, 5): + print("This module requires Python >= 3.5") sys.exit(0) # Check whether we are compiling for PyPy. Headers will not be installed @@ -913,7 +913,6 @@ def use_educated_guess(self): "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", @@ -932,10 +931,4 @@ def use_educated_guess(self): }, ) -if sys.version_info <= (2, 7): - options["requires"] = options.pop("install_requires") - -if sys.version_info > (3, 0): - options["use_2to3"] = True - setup(**options) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index eb200c1ad..c87bfa415 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -7,9 +7,8 @@ _3d_version_for """ -from __future__ import with_statement -__license__ = u""" +__license__ = """ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -54,15 +53,17 @@ import operator from collections import defaultdict -from itertools import izip + from shutil import copyfileobj from warnings import warn + def deprecated(message): """Prints a warning message related to the deprecation of some igraph feature.""" warn(message, DeprecationWarning, stacklevel=3) + # pylint: disable-msg=E1101 class Graph(GraphBase): """Generic graph. @@ -167,8 +168,12 @@ def __init__(self, *args, **kwds): # Set up default values for the parameters. This should match the order # in *args kwd_order = ( - "n", "edges", "directed", "graph_attrs", "vertex_attrs", - "edge_attrs" + "n", + "edges", + "directed", + "graph_attrs", + "vertex_attrs", + "edge_attrs", ) params = [0, [], False, {}, {}, {}] @@ -177,9 +182,11 @@ def __init__(self, *args, **kwds): unknown_kwds = set(kwds.keys()) unknown_kwds.difference_update(kwd_order) if unknown_kwds: - raise TypeError("{0}.__init__ got an unexpected keyword argument {1!r}".format( - self.__class__.__name__, unknown_kwds.pop() - )) + raise TypeError( + "{0}.__init__ got an unexpected keyword argument {1!r}".format( + self.__class__.__name__, unknown_kwds.pop() + ) + ) # If the first argument is a list or any other iterable, assume that # the number of vertices were omitted @@ -188,7 +195,7 @@ def __init__(self, *args, **kwds): args.insert(0, params[0]) # Override default parameters from args - params[:len(args)] = args + params[: len(args)] = args # Override default parameters from keywords for idx, k in enumerate(kwd_order): @@ -210,6 +217,7 @@ def __init__(self, *args, **kwds): # as the lower-level C API works with memoryviews only try: from numpy import ndarray, matrix + if isinstance(edges, (ndarray, matrix)): edges = numpy_to_contiguous_memoryview(edges) except ImportError: @@ -222,18 +230,18 @@ def __init__(self, *args, **kwds): GraphBase.__init__(self, nverts, edges, directed) # Set the graph attributes - for key, value in graph_attrs.iteritems(): - if isinstance(key, (int, long)): + for key, value in graph_attrs.items(): + if isinstance(key, int): key = str(key) self[key] = value # Set the vertex attributes - for key, value in vertex_attrs.iteritems(): - if isinstance(key, (int, long)): + for key, value in vertex_attrs.items(): + if isinstance(key, int): key = str(key) self.vs[key] = value # Set the edge attributes - for key, value in edge_attrs.iteritems(): - if isinstance(key, (int, long)): + for key, value in edge_attrs.items(): + if isinstance(key, int): key = str(key) self.es[key] = value @@ -255,7 +263,7 @@ def add_edge(self, source, target, **kwds): eid = self.ecount() self.add_edges([(source, target)]) edge = self.es[eid] - for key, value in kwds.iteritems(): + for key, value in kwds.items(): edge[key] = value return edge @@ -275,7 +283,7 @@ def add_edges(self, es, attributes=None): res = GraphBase.add_edges(self, es) n = self.ecount() - eid if (attributes is not None) and (n > 0): - for key, val in attributes.items(): + for key, val in list(attributes.items()): self.es[eid:][key] = val return res @@ -295,7 +303,7 @@ def add_vertex(self, name=None, **kwds): vid = self.vcount() self.add_vertices(1) vertex = self.vs[vid] - for key, value in kwds.iteritems(): + for key, value in kwds.items(): vertex[key] = value if name is not None: vertex["name"] = name @@ -320,13 +328,13 @@ def add_vertices(self, n, attributes=None): values of this dict are the attributes themselves, but if n=1 then they have to be lists of length 1. """ - if isinstance(n, basestring): + if isinstance(n, str): # Adding a single vertex with a name m = self.vcount() result = GraphBase.add_vertices(self, 1) self.vs[m]["name"] = n if attributes is not None: - for key, val in attributes.items(): + for key, val in list(attributes.items()): self.vs[m][key] = val elif hasattr(n, "__iter__"): m = self.vcount() @@ -338,13 +346,13 @@ def add_vertices(self, n, attributes=None): if len(names) > 0: self.vs[m:]["name"] = names if attributes is not None: - for key, val in attributes.items(): + for key, val in list(attributes.items()): self.vs[m:][key] = val else: result = GraphBase.add_vertices(self, n) if (attributes is not None) and (n > 0): m = self.vcount() - n - for key, val in attributes.items(): + for key, val in list(attributes.items()): self.vs[m:][key] = val return result @@ -355,8 +363,10 @@ def adjacent(self, *args, **kwds): @deprecated: replaced by L{Graph.incident()} since igraph 0.6 """ - deprecated("Graph.adjacent() is deprecated since igraph 0.6, please use " - "Graph.incident() instead") + deprecated( + "Graph.adjacent() is deprecated since igraph 0.6, please use " + "Graph.incident() instead" + ) return self.incident(*args, **kwds) def as_directed(self, *args, **kwds): @@ -414,7 +424,7 @@ def indegree(self, *args, **kwds): See L{degree} for possible arguments. """ - kwds['mode'] = IN + kwds["mode"] = IN return self.degree(*args, **kwds) def outdegree(self, *args, **kwds): @@ -422,7 +432,7 @@ def outdegree(self, *args, **kwds): See L{degree} for possible arguments. """ - kwds['mode'] = OUT + kwds["mode"] = OUT return self.degree(*args, **kwds) def all_st_cuts(self, source, target): @@ -441,8 +451,10 @@ def all_st_cuts(self, source, target): @ref: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in graphs. Algorithmica 15, 351--372, 1996. """ - return [Cut(self, cut=cut, partition=part) - for cut, part in izip(*GraphBase.all_st_cuts(self, source, target))] + return [ + Cut(self, cut=cut, partition=part) + for cut, part in zip(*GraphBase.all_st_cuts(self, source, target)) + ] def all_st_mincuts(self, source, target, capacity=None): """\ @@ -463,8 +475,9 @@ def all_st_mincuts(self, source, target, capacity=None): graphs. Algorithmica 15, 351--372, 1996. """ value, cuts, parts = GraphBase.all_st_mincuts(self, source, target, capacity) - return [Cut(self, value, cut=cut, partition=part) - for cut, part in izip(cuts, parts)] + return [ + Cut(self, value, cut=cut, partition=part) for cut, part in zip(cuts, parts) + ] def biconnected_components(self, return_articulation_points=False): """\ @@ -495,6 +508,7 @@ def biconnected_components(self, return_articulation_points=False): return clustering, aps else: return clustering + blocks = biconnected_components def clear(self): @@ -538,9 +552,10 @@ def clusters(self, mode=STRONG): clusters being sought. Optional, defaults to C{STRONG}. @return: a L{VertexClustering} object""" return VertexClustering(self, GraphBase.clusters(self, mode)) + components = clusters - def degree_distribution(self, bin_width = 1, *args, **kwds): + def degree_distribution(self, bin_width=1, *args, **kwds): """degree_distribution(bin_width=1, ...) Calculates the degree distribution of the graph. @@ -573,8 +588,9 @@ def dyad_census(self, *args, **kwds): """ return DyadCensus(GraphBase.dyad_census(self, *args, **kwds)) - def get_adjacency(self, type=GET_ADJACENCY_BOTH, attribute=None, \ - default=0, eids=False): + def get_adjacency( + self, type=GET_ADJACENCY_BOTH, attribute=None, default=0, eids=False + ): """Returns the adjacency matrix of a graph. @param type: either C{GET_ADJACENCY_LOWER} (uses the lower @@ -599,8 +615,11 @@ def get_adjacency(self, type=GET_ADJACENCY_BOTH, attribute=None, \ in the matrix for each vertex pair. @return: the adjacency matrix as a L{Matrix}. """ - if type != GET_ADJACENCY_LOWER and type != GET_ADJACENCY_UPPER and \ - type != GET_ADJACENCY_BOTH: + if ( + type != GET_ADJACENCY_LOWER + and type != GET_ADJACENCY_UPPER + and type != GET_ADJACENCY_BOTH + ): # Maybe it was called with the first argument as the attribute name type, attribute = attribute, type if type is None: @@ -617,7 +636,7 @@ def get_adjacency(self, type=GET_ADJACENCY_BOTH, attribute=None, \ if attribute not in self.es.attribute_names(): raise ValueError("Attribute does not exist") - data = [[default] * self.vcount() for _ in xrange(self.vcount())] + data = [[default] * self.vcount() for _ in range(self.vcount())] if self.is_directed(): for edge in self.es: @@ -651,7 +670,9 @@ def get_adjacency_sparse(self, attribute=None): try: from scipy import sparse except ImportError: - raise ImportError('You should install scipy package in order to use this function') + raise ImportError( + "You should install scipy package in order to use this function" + ) import numpy as np edges = self.get_edgelist() @@ -664,7 +685,7 @@ def get_adjacency_sparse(self, attribute=None): weights = self.es[attribute] N = self.vcount() - mtx = sparse.csr_matrix((weights, zip(*edges)), shape=(N, N)) + mtx = sparse.csr_matrix((weights, list(zip(*edges))), shape=(N, N)) if not self.is_directed(): mtx = mtx + sparse.triu(mtx, 1).T + sparse.tril(mtx, -1).T @@ -684,7 +705,7 @@ def get_adjlist(self, mode=OUT): the predecessors and the successors will be returned. Ignored for undirected graphs. """ - return [self.neighbors(idx, mode) for idx in xrange(self.vcount())] + return [self.neighbors(idx, mode) for idx in range(self.vcount())] def get_adjedgelist(self, *args, **kwds): """get_adjedgelist(mode=OUT) @@ -694,8 +715,10 @@ def get_adjedgelist(self, *args, **kwds): @deprecated: replaced by L{Graph.get_inclist()} since igraph 0.6 @see: Graph.get_inclist() """ - deprecated("Graph.get_adjedgelist() is deprecated since igraph 0.6, " - "please use Graph.get_inclist() instead") + deprecated( + "Graph.get_adjedgelist() is deprecated since igraph 0.6, " + "please use Graph.get_inclist() instead" + ) return self.get_inclist(*args, **kwds) def get_all_simple_paths(self, v, to=None, cutoff=-1, mode=OUT): @@ -731,7 +754,7 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode=OUT): for index, item in enumerate(paths): if item < 0: result.append(paths[prev:index]) - prev = index+1 + prev = index + 1 return result def get_inclist(self, mode=OUT): @@ -749,7 +772,7 @@ def get_inclist(self, mode=OUT): the predecessors and the successors will be returned. Ignored for undirected graphs. """ - return [self.incident(idx, mode) for idx in xrange(self.vcount())] + return [self.incident(idx, mode) for idx in range(self.vcount())] def gomory_hu_tree(self, capacity=None, flow="flow"): """gomory_hu_tree(capacity=None, flow="flow") @@ -917,13 +940,21 @@ def path_length_hist(self, directed=True): data, unconn = GraphBase.path_length_hist(self, directed) hist = Histogram(bin_width=1) for i, length in enumerate(data): - hist.add(i+1, length) - hist.unconnected = long(unconn) + hist.add(i + 1, length) + hist.unconnected = int(unconn) return hist - def pagerank(self, vertices=None, directed=True, damping=0.85, - weights=None, arpack_options=None, implementation="prpack", - niter=1000, eps=0.001): + def pagerank( + self, + vertices=None, + directed=True, + damping=0.85, + weights=None, + arpack_options=None, + implementation="prpack", + niter=1000, + eps=0.001, + ): """Calculates the Google PageRank values of a graph. @param vertices: the indices of the vertices being queried. @@ -957,9 +988,18 @@ def pagerank(self, vertices=None, directed=True, damping=0.85, vertices.""" if arpack_options is None: arpack_options = _igraph.arpack_options - return self.personalized_pagerank(vertices, directed, damping, None,\ - None, weights, arpack_options, \ - implementation, niter, eps) + return self.personalized_pagerank( + vertices, + directed, + damping, + None, + None, + weights, + arpack_options, + implementation, + niter, + eps, + ) def spanning_tree(self, weights=None, return_tree=True): """Calculates a minimum spanning tree for a graph. @@ -1037,8 +1077,9 @@ def triad_census(self, *args, **kwds): return TriadCensus(GraphBase.triad_census(self, *args, **kwds)) # Automorphisms - def count_automorphisms_vf2(self, color=None, edge_color=None, - node_compat_fn=None, edge_compat_fn=None): + def count_automorphisms_vf2( + self, color=None, edge_color=None, node_compat_fn=None, edge_compat_fn=None + ): """Returns the number of automorphisms of the graph. This function simply calls C{count_isomorphisms_vf2} with the graph @@ -1048,12 +1089,19 @@ def count_automorphisms_vf2(self, color=None, edge_color=None, @return: the number of automorphisms of the graph @see: Graph.count_isomorphisms_vf2 """ - return self.count_isomorphisms_vf2(self, color1=color, color2=color, - edge_color1=edge_color, edge_color2=edge_color, - node_compat_fn=node_compat_fn, edge_compat_fn=edge_compat_fn) + return self.count_isomorphisms_vf2( + self, + color1=color, + color2=color, + edge_color1=edge_color, + edge_color2=edge_color, + node_compat_fn=node_compat_fn, + edge_compat_fn=edge_compat_fn, + ) - def get_automorphisms_vf2(self, color=None, edge_color=None, - node_compat_fn=None, edge_compat_fn=None): + def get_automorphisms_vf2( + self, color=None, edge_color=None, node_compat_fn=None, edge_compat_fn=None + ): """Returns all the automorphisms of the graph This function simply calls C{get_isomorphisms_vf2} with the graph @@ -1064,9 +1112,15 @@ def get_automorphisms_vf2(self, color=None, edge_color=None, of the graph vertices to itself according to the automorphism @see: Graph.get_isomorphisms_vf2 """ - return self.get_isomorphisms_vf2(self, color1=color, color2=color, - edge_color1=edge_color, edge_color2=edge_color, - node_compat_fn=node_compat_fn, edge_compat_fn=edge_compat_fn) + return self.get_isomorphisms_vf2( + self, + color1=color, + color2=color, + edge_color1=edge_color, + edge_color2=edge_color, + node_compat_fn=node_compat_fn, + edge_compat_fn=edge_compat_fn, + ) # Various clustering algorithms -- mostly wrappers around GraphBase def community_fastgreedy(self, weights=None): @@ -1098,8 +1152,9 @@ def community_fastgreedy(self, weights=None): else: optimal_count = diff - return VertexDendrogram(self, merges, optimal_count, - modularity_params=dict(weights=weights)) + return VertexDendrogram( + self, merges, optimal_count, modularity_params=dict(weights=weights) + ) def community_infomap(self, edge_weights=None, vertex_weights=None, trials=10): """Finds the community structure of the network according to the Infomap @@ -1124,14 +1179,17 @@ def community_infomap(self, edge_weights=None, vertex_weights=None, trials=10): U{http://dx.doi.org/10.1140/epjst/e2010-01179-1}, U{http://arxiv.org/abs/0906.1405}. """ - membership, codelength = \ - GraphBase.community_infomap(self, edge_weights, vertex_weights, trials) - return VertexClustering(self, membership, \ - params={"codelength": codelength}, \ - modularity_params={"weights": edge_weights} ) - - def community_leading_eigenvector_naive(self, clusters = None, \ - return_merges = False): + membership, codelength = GraphBase.community_infomap( + self, edge_weights, vertex_weights, trials + ) + return VertexClustering( + self, + membership, + params={"codelength": codelength}, + modularity_params={"weights": edge_weights}, + ) + + def community_leading_eigenvector_naive(self, clusters=None, return_merges=False): """community_leading_eigenvector_naive(clusters=None, return_merges=False) @@ -1158,16 +1216,17 @@ def community_leading_eigenvector_naive(self, clusters = None, \ eigenvectors of matrices, arXiv:physics/0605087""" if clusters is None: clusters = -1 - cl, merges, q = GraphBase.community_leading_eigenvector_naive(self, \ - clusters, return_merges) + cl, merges, q = GraphBase.community_leading_eigenvector_naive( + self, clusters, return_merges + ) if merges is None: - return VertexClustering(self, cl, modularity = q) + return VertexClustering(self, cl, modularity=q) else: - return VertexDendrogram(self, merges, safemax(cl)+1) + return VertexDendrogram(self, merges, safemax(cl) + 1) - - def community_leading_eigenvector(self, clusters=None, weights=None, \ - arpack_options=None): + def community_leading_eigenvector( + self, clusters=None, weights=None, arpack_options=None + ): """community_leading_eigenvector(clusters=None, weights=None, arpack_options=None) @@ -1198,12 +1257,12 @@ def community_leading_eigenvector(self, clusters=None, weights=None, \ if arpack_options is not None: kwds["arpack_options"] = arpack_options - membership, _, q = GraphBase.community_leading_eigenvector(self, clusters, **kwds) - return VertexClustering(self, membership, modularity = q) - + membership, _, q = GraphBase.community_leading_eigenvector( + self, clusters, **kwds + ) + return VertexClustering(self, membership, modularity=q) - def community_label_propagation(self, weights = None, initial = None, \ - fixed = None): + def community_label_propagation(self, weights=None, initial=None, fixed=None): """community_label_propagation(weights=None, initial=None, fixed=None) Finds the community structure of the graph according to the label @@ -1236,13 +1295,10 @@ def community_label_propagation(self, weights = None, initial = None, \ networks. Phys Rev E 76:036106, 2007. U{http://arxiv.org/abs/0709.2938}. """ - if isinstance(fixed, basestring): + if isinstance(fixed, str): fixed = [bool(o) for o in g.vs[fixed]] - cl = GraphBase.community_label_propagation(self, \ - weights, initial, fixed) - return VertexClustering(self, cl, - modularity_params=dict(weights=weights)) - + cl = GraphBase.community_label_propagation(self, weights, initial, fixed) + return VertexClustering(self, cl, modularity_params=dict(weights=weights)) def community_multilevel(self, weights=None, return_levels=False): """Community structure based on the multilevel algorithm of @@ -1281,12 +1337,16 @@ def community_multilevel(self, weights=None, return_levels=False): levels, qs = GraphBase.community_multilevel(self, weights, True) result = [] for level, q in zip(levels, qs): - result.append(VertexClustering(self, level, q, - modularity_params=dict(weights=weights))) + result.append( + VertexClustering( + self, level, q, modularity_params=dict(weights=weights) + ) + ) else: membership = GraphBase.community_multilevel(self, weights, False) - result = VertexClustering(self, membership, - modularity_params=dict(weights=weights)) + result = VertexClustering( + self, membership, modularity_params=dict(weights=weights) + ) return result def community_optimal_modularity(self, *args, **kwds): @@ -1302,12 +1362,12 @@ def community_optimal_modularity(self, *args, **kwds): @return: the calculated membership vector and the corresponding modularity in a tuple.""" - membership, modularity = \ - GraphBase.community_optimal_modularity(self, *args, **kwds) + membership, modularity = GraphBase.community_optimal_modularity( + self, *args, **kwds + ) return VertexClustering(self, membership, modularity) - def community_edge_betweenness(self, clusters=None, directed=True, - weights=None): + def community_edge_betweenness(self, clusters=None, directed=True, weights=None): """Community structure based on the betweenness of the edges in the network. @@ -1338,11 +1398,12 @@ def community_edge_betweenness(self, clusters=None, directed=True, qs.reverse() if clusters is None: if qs: - clusters = qs.index(max(qs))+1 + clusters = qs.index(max(qs)) + 1 else: clusters = 1 - return VertexDendrogram(self, merges, clusters, - modularity_params=dict(weights=weights)) + return VertexDendrogram( + self, merges, clusters, modularity_params=dict(weights=weights) + ) def community_spinglass(self, *args, **kwds): """community_spinglass(weights=None, spins=25, parupdate=False, @@ -1397,11 +1458,10 @@ def community_spinglass(self, *args, **kwds): """ membership = GraphBase.community_spinglass(self, *args, **kwds) if "weights" in kwds: - modularity_params=dict(weights=kwds["weights"]) + modularity_params = dict(weights=kwds["weights"]) else: - modularity_params={} - return VertexClustering(self, membership, - modularity_params=modularity_params) + modularity_params = {} + return VertexClustering(self, membership, modularity_params=modularity_params) def community_walktrap(self, weights=None, steps=4): """Community detection algorithm of Latapy & Pons, based on random @@ -1425,11 +1485,12 @@ def community_walktrap(self, weights=None, steps=4): merges, qs = GraphBase.community_walktrap(self, weights, steps) qs.reverse() if qs: - optimal_count = qs.index(max(qs))+1 + optimal_count = qs.index(max(qs)) + 1 else: optimal_count = 1 - return VertexDendrogram(self, merges, optimal_count, - modularity_params=dict(weights=weights)) + return VertexDendrogram( + self, merges, optimal_count, modularity_params=dict(weights=weights) + ) def k_core(self, *args): """Returns some k-cores of the graph. @@ -1443,7 +1504,7 @@ def k_core(self, *args): all M{k}-cores in increasing order of M{k}. """ if len(args) == 0: - indices = xrange(self.vcount()) + indices = range(self.vcount()) return_single = False else: return_single = True @@ -1454,22 +1515,30 @@ def k_core(self, *args): except: indices.append(arg) - if len(indices)>1 or hasattr(args[0], "__iter__"): + if len(indices) > 1 or hasattr(args[0], "__iter__"): return_single = False corenesses = self.coreness() result = [] - vidxs = xrange(self.vcount()) + vidxs = range(self.vcount()) for idx in indices: core_idxs = [vidx for vidx in vidxs if corenesses[vidx] >= idx] result.append(self.subgraph(core_idxs)) - if return_single: return result[0] + if return_single: + return result[0] return result - def community_leiden(self, objective_function="CPM", weights=None, - resolution_parameter=1.0, beta=0.01, initial_membership=None, - n_iterations=2, node_weights=None): + def community_leiden( + self, + objective_function="CPM", + weights=None, + resolution_parameter=1.0, + beta=0.01, + initial_membership=None, + n_iterations=2, + node_weights=None, + ): """community_leiden(objective_function=CPM, weights=None, resolution_parameter=1.0, beta=0.01, initial_membership=None, n_iterations=2, node_weights=None) @@ -1506,20 +1575,24 @@ def community_leiden(self, objective_function="CPM", weights=None, reports, 9(1), 5233. doi: 10.1038/s41598-019-41695-z """ if objective_function.lower() not in ("cpm", "modularity"): - raise ValueError("objective_function must be \"CPM\" or \"modularity\".") + raise ValueError('objective_function must be "CPM" or "modularity".') - membership = GraphBase.community_leiden(self, - edge_weights=weights, node_weights=node_weights, - resolution_parameter=resolution_parameter, - normalize_resolution=(objective_function == "modularity"), - beta=beta, initial_membership=initial_membership, n_iterations=n_iterations) + membership = GraphBase.community_leiden( + self, + edge_weights=weights, + node_weights=node_weights, + resolution_parameter=resolution_parameter, + normalize_resolution=(objective_function == "modularity"), + beta=beta, + initial_membership=initial_membership, + n_iterations=n_iterations, + ) if weights is not None: - modularity_params=dict(weights=weights) + modularity_params = dict(weights=weights) else: - modularity_params={} - return VertexClustering(self, membership, - modularity_params=modularity_params) + modularity_params = {} + return VertexClustering(self, membership, modularity_params=modularity_params) def layout(self, layout=None, *args, **kwds): """Returns the layout of the graph according to a layout algorithm. @@ -1668,9 +1741,9 @@ def layout_auto(self, *args, **kwds): vattrs = self.vertex_attributes() if "x" in vattrs and "y" in vattrs: if dim == 3 and "z" in vattrs: - return Layout(zip(self.vs["x"], self.vs["y"], self.vs["z"])) + return Layout(list(zip(self.vs["x"], self.vs["y"], self.vs["z"]))) else: - return Layout(zip(self.vs["x"], self.vs["y"])) + return Layout(list(zip(self.vs["x"], self.vs["y"]))) if self.vcount() <= 100 and self.is_connected(): algo = "kk" @@ -1688,13 +1761,22 @@ def layout_grid_fruchterman_reingold(self, *args, **kwds): @see: Graph.layout_fruchterman_reingold() """ - deprecated("Graph.layout_grid_fruchterman_reingold() is deprecated since "\ - "igraph 0.8, please use Graph.layout_fruchterman_reingold(grid=True) instead") + deprecated( + "Graph.layout_grid_fruchterman_reingold() is deprecated since " + "igraph 0.8, please use Graph.layout_fruchterman_reingold(grid=True) instead" + ) kwds["grid"] = True return self.layout_fruchterman_reingold(*args, **kwds) - def layout_sugiyama(self, layers=None, weights=None, hgap=1, vgap=1, - maxiter=100, return_extended_graph=False): + def layout_sugiyama( + self, + layers=None, + weights=None, + hgap=1, + vgap=1, + maxiter=100, + return_extended_graph=False, + ): """layout_sugiyama(layers=None, weights=None, hgap=1, vgap=1, maxiter=100, return_extended_graph=False) @@ -1751,12 +1833,15 @@ def layout_sugiyama(self, layers=None, weights=None, hgap=1, vgap=1, feedback arc set problem. Information Processing Letters 47:319-323, 1993. """ if not return_extended_graph: - return Layout(GraphBase._layout_sugiyama(self, layers, weights, hgap, - vgap, maxiter, return_extended_graph)) - - layout, extd_graph, extd_to_orig_eids = \ - GraphBase._layout_sugiyama(self, layers, weights, hgap, - vgap, maxiter, return_extended_graph) + return Layout( + GraphBase._layout_sugiyama( + self, layers, weights, hgap, vgap, maxiter, return_extended_graph + ) + ) + + layout, extd_graph, extd_to_orig_eids = GraphBase._layout_sugiyama( + self, layers, weights, hgap, vgap, maxiter, return_extended_graph + ) extd_graph.es["_original_eid"] = extd_to_orig_eids return Layout(layout), extd_graph @@ -1836,21 +1921,19 @@ def from_networkx(klass, g): # Nodes vnames = list(g.nodes) - vattr = {'_nx_name': vnames} + vattr = {"_nx_name": vnames} vcount = len(vnames) vd = {v: i for i, v in enumerate(vnames)} # NOTE: we do not need a special class for multigraphs, it is taken # care for at the edge level rather than at the graph level. graph = klass( - n=vcount, - directed=g.is_directed(), - graph_attrs=gattr, - vertex_attrs=vattr) + n=vcount, directed=g.is_directed(), graph_attrs=gattr, vertex_attrs=vattr + ) # Node attributes for v, datum in g.nodes.data(): - for key, val in datum.items(): + for key, val in list(datum.items()): graph.vs[vd[v]][key] = val # Edges and edge attributes @@ -1859,16 +1942,14 @@ def from_networkx(klass, g): # third element is the "color" of the edge) for e, (_, _, datum) in zip(g.edges, g.edges.data()): eid = graph.add_edge(vd[e[0]], vd[e[1]]) - for key, val in datum.items(): + for key, val in list(datum.items()): eid[key] = val return graph def to_graph_tool( - self, - graph_attributes=None, - vertex_attributes=None, - edge_attributes=None): + self, graph_attributes=None, vertex_attributes=None, edge_attributes=None + ): """Converts the graph to graph-tool Data types: graph-tool only accepts specific data types. See the @@ -1904,7 +1985,7 @@ def to_graph_tool( # Graph attributes if graph_attributes is not None: - for x, dtype in graph_attributes.items(): + for x, dtype in list(graph_attributes.items()): # Strange syntax for setting internal properties gprop = g.new_graph_property(str(dtype)) g.graph_properties[x] = gprop @@ -1912,7 +1993,7 @@ def to_graph_tool( # Vertex attributes if vertex_attributes is not None: - for x, dtype in vertex_attributes.items(): + for x, dtype in list(vertex_attributes.items()): # Create a new vertex property g.vertex_properties[x] = g.new_vertex_property(str(dtype)) # Fill the values from the igraph.Graph @@ -1921,12 +2002,12 @@ def to_graph_tool( # Edges and edge attributes if edge_attributes is not None: - for x, dtype in edge_attributes.items(): + for x, dtype in list(edge_attributes.items()): g.edge_properties[x] = g.new_edge_property(str(dtype)) for edge in self.es: e = g.add_edge(edge.source, edge.target) if edge_attributes is not None: - for x, dtype in edge_attributes.items(): + for x, dtype in list(edge_attributes.items()): prop = edge.attributes().get(x, None) g.edge_properties[x][e] = prop @@ -1945,13 +2026,10 @@ def from_graph_tool(klass, g): vcount = g.num_vertices() # Graph - graph = klass( - n=vcount, - directed=g.is_directed(), - graph_attrs=gattr) + graph = klass(n=vcount, directed=g.is_directed(), graph_attrs=gattr) # Node attributes - for key, val in g.vertex_properties.items(): + for key, val in list(g.vertex_properties.items()): prop = val.get_array() for i in range(vcount): graph.vs[i][key] = prop[i] @@ -1961,7 +2039,7 @@ def from_graph_tool(klass, g): # attributes later on for e in g.edges(): edge = graph.add_edge(int(e.source()), int(e.target())) - for key, val in g.edge_properties.items(): + for key, val in list(g.edge_properties.items()): edge[key] = val[e] return graph @@ -1978,7 +2056,7 @@ def write_adjacency(self, f, sep=" ", eol="\n", *args, **kwds): note that igraph is able to read back the written adjacency matrix if and only if this is a single newline character """ - if isinstance(f, basestring): + if isinstance(f, str): f = open(f, "w") matrix = self.get_adjacency(*args, **kwds) for row in matrix: @@ -1987,8 +2065,9 @@ def write_adjacency(self, f, sep=" ", eol="\n", *args, **kwds): f.close() @classmethod - def Read_Adjacency(klass, f, sep=None, comment_char = "#", attribute=None, - *args, **kwds): + def Read_Adjacency( + klass, f, sep=None, comment_char="#", attribute=None, *args, **kwds + ): """Constructs a graph based on an adjacency matrix from the given file Additional positional and keyword arguments not mentioned here are @@ -2004,13 +2083,15 @@ def Read_Adjacency(klass, f, sep=None, comment_char = "#", attribute=None, no weights are stored, values larger than 1 are considered as edge multiplicities. @return: the created graph""" - if isinstance(f, basestring): + if isinstance(f, str): f = open(f) matrix, ri, weights = [], 0, {} for line in f: line = line.strip() - if len(line) == 0: continue - if line.startswith(comment_char): continue + if len(line) == 0: + continue + if line.startswith(comment_char): + continue row = [float(x) for x in line.split(sep)] matrix.append(row) ri += 1 @@ -2018,10 +2099,10 @@ def Read_Adjacency(klass, f, sep=None, comment_char = "#", attribute=None, f.close() if attribute is None: - graph=klass.Adjacency(matrix, *args, **kwds) + graph = klass.Adjacency(matrix, *args, **kwds) else: kwds["attr"] = attribute - graph=klass.Weighted_Adjacency(matrix, *args, **kwds) + graph = klass.Weighted_Adjacency(matrix, *args, **kwds) return graph @@ -2043,7 +2124,8 @@ def write_dimacs(self, f, source=None, target=None, capacity="capacity"): except KeyError: raise ValueError( "source vertex must be provided in the 'source' graph " - "attribute or in the 'source' argument of write_dimacs()") + "attribute or in the 'source' argument of write_dimacs()" + ) if target is None: try: @@ -2051,10 +2133,10 @@ def write_dimacs(self, f, source=None, target=None, capacity="capacity"): except KeyError: raise ValueError( "target vertex must be provided in the 'target' graph " - "attribute or in the 'target' argument of write_dimacs()") + "attribute or in the 'target' argument of write_dimacs()" + ) - if isinstance(capacity, basestring) and \ - capacity not in self.edge_attributes(): + if isinstance(capacity, str) and capacity not in self.edge_attributes(): warn("'%s' edge attribute does not exist" % capacity) capacity = [1] * self.ecount() @@ -2077,6 +2159,7 @@ def write_graphmlz(self, f, compresslevel=9): produces the least compression, and 9 is slowest and produces the most compression.""" from igraph.utils import named_temporary_file + with named_temporary_file() as tmpfile: self.write_graphml(tmpfile) outf = gzip.GzipFile(f, "wb", compresslevel) @@ -2129,6 +2212,7 @@ def Read_GraphMLz(cls, f, *params, **kwds): specify 0 here. @return: the loaded graph object""" from igraph.utils import named_temporary_file + with named_temporary_file() as tmpfile: outf = open(tmpfile, "wb") copyfileobj(gzip.GzipFile(f, "rb"), outf) @@ -2145,15 +2229,16 @@ def write_pickle(self, fname=None, version=-1): @return: C{None} if the graph was saved successfully to the given file, or a string if C{fname} was C{None}. """ - import cPickle as pickle + import pickle as pickle + if fname is None: return pickle.dumps(self, version) if not hasattr(fname, "write"): file_was_opened = True - fname = open(fname, 'wb') + fname = open(fname, "wb") else: - file_was_opened=False - result=pickle.dump(self, fname, version) + file_was_opened = False + result = pickle.dump(self, fname, version) if file_was_opened: fname.close() return result @@ -2172,7 +2257,8 @@ def write_picklez(self, fname=None, version=-1): @return: C{None} if the graph was saved successfully to the given file. """ - import cPickle as pickle + import pickle as pickle + if not hasattr(fname, "write"): file_was_opened = True fname = gzip.open(fname, "wb") @@ -2194,7 +2280,8 @@ def Read_Pickle(klass, fname=None): a string containing the pickled data. @return: the created graph object. """ - import cPickle as pickle + import pickle as pickle + if hasattr(fname, "read"): # Probably a file or a file-like object result = pickle.load(fname) @@ -2209,13 +2296,17 @@ def Read_Pickle(klass, fname=None): # directly. result = pickle.loads(fname) except TypeError: - raise IOError('Cannot load file. If fname is a file name, that filename may be incorrect.') + raise IOError( + "Cannot load file. If fname is a file name, that filename may be incorrect." + ) except IOError: try: # No file with the given name, try unpickling directly. result = pickle.loads(fname) except TypeError: - raise IOError('Cannot load file. If fname is a file name, that filename may be incorrect.') + raise IOError( + "Cannot load file. If fname is a file name, that filename may be incorrect." + ) if fp is not None: result = pickle.load(fp) fp.close() @@ -2229,7 +2320,8 @@ def Read_Picklez(klass, fname, *args, **kwds): @param fname: the name of the file or a stream to read from. @return: the created graph object. """ - import cPickle as pickle + import pickle as pickle + if hasattr(fname, "read"): # Probably a file or a file-like object if isinstance(fname, gzip.GzipFile): @@ -2248,7 +2340,8 @@ def Read_Picklez(klass, fname, *args, **kwds): @param fname: the name of the file or a stream to read from. @return: the created graph object. """ - import cPickle as pickle + import pickle as pickle + if hasattr(fname, "read"): # Probably a file or a file-like object if isinstance(fname, gzip.GzipFile): @@ -2264,11 +2357,22 @@ def Read_Picklez(klass, fname, *args, **kwds): # pylint: disable-msg=C0301,C0323 # C0301: line too long. # C0323: operator not followed by a space - well, print >>f looks OK - def write_svg(self, fname, layout="auto", width=None, height=None, \ - labels="label", colors="color", shapes="shape", \ - vertex_size=10, edge_colors="color", \ - edge_stroke_widths="width", \ - font_size=16, *args, **kwds): + def write_svg( + self, + fname, + layout="auto", + width=None, + height=None, + labels="label", + colors="color", + shapes="shape", + vertex_size=10, + edge_colors="color", + edge_stroke_widths="width", + font_size=16, + *args, + **kwds + ): """Saves the graph as an SVG (Scalable Vector Graphics) file The file will be Inkscape (http://inkscape.org) compatible. @@ -2327,7 +2431,7 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ try: labels = self.vs.get_attribute_values(labels) except KeyError: - labels = [x+1 for x in xrange(self.vcount())] + labels = [x + 1 for x in range(self.vcount())] elif labels is None: labels = [""] * self.vcount() @@ -2362,10 +2466,10 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ raise ValueError("font size can't contain a semicolon") vcount = self.vcount() - labels.extend(str(i+1) for i in xrange(len(labels), vcount)) + labels.extend(str(i + 1) for i in range(len(labels), vcount)) colors.extend(["red"] * (vcount - len(colors))) - if isinstance(fname, basestring): + if isinstance(fname, str): f = open(fname, "w") our_file = True else: @@ -2374,7 +2478,7 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ bbox = BoundingBox(layout.bounding_box()) - sizes = [width-2*vertex_size, height-2*vertex_size] + sizes = [width - 2 * vertex_size, height - 2 * vertex_size] w, h = bbox.width, bbox.height ratios = [] @@ -2387,21 +2491,30 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ else: ratios.append(sizes[1] / h) - layout = [[(row[0] - bbox.left) * ratios[0] + vertex_size, \ - (row[1] - bbox.top) * ratios[1] + vertex_size] \ - for row in layout] + layout = [ + [ + (row[0] - bbox.left) * ratios[0] + vertex_size, + (row[1] - bbox.top) * ratios[1] + vertex_size, + ] + for row in layout + ] directed = self.is_directed() - print >> f, '' - print >> f, '' - print >> f - print >> f, '> f, 'width="{0}px" height="{1}px">'.format(width, height), - + print('', file=f) + print( + "", + file=f, + ) + print(file=f) + print( + ''.format(width, height), end=" ", file=f) edge_color_dict = {} - print >> f, '' + print('', file=f) for e_col in set(edge_colors): if e_col == "#000000": marker_index = "" @@ -2409,23 +2522,34 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ marker_index = str(len(edge_color_dict)) # Print an arrow marker for each possible line color # This is a copy of Inkscape's standard Arrow 2 marker - print >> f, '> f, ' inkscape:stockid="Arrow2Lend{0}"'.format(marker_index) - print >> f, ' orient="auto"' - print >> f, ' refY="0.0"' - print >> f, ' refX="0.0"' - print >> f, ' id="Arrow2Lend{0}"'.format(marker_index) - print >> f, ' style="overflow:visible;">' - print >> f, ' > f, ' id="pathArrow{0}"'.format(marker_index) - print >> f, ' style="font-size:12.0;fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;fill:{0}"'.format(e_col) - print >> f, ' d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "' - print >> f, ' transform="scale(1.1) rotate(180) translate(1,0)" />' - print >> f, '' + print("', file=f) + print(" ', file=f) + print("", file=f) edge_color_dict[e_col] = "Arrow2Lend{0}".format(marker_index) - print >> f, '' - print >> f, '' + print("", file=f) + print( + '', + file=f, + ) for eidx, edge in enumerate(self.es): vidxs = edge.tuple @@ -2437,32 +2561,41 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ x2 = x2 - vertex_size * math.cos(angle) y2 = y2 - vertex_size * math.sin(angle) - print >> f, '> f, ' style="fill:none;stroke:{0};stroke-width:{2};stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none{1}"'\ - .format(edge_colors[eidx], ";marker-end:url(#{0})".\ - format(edge_color_dict[edge_colors[eidx]]) \ - if directed else "", edge_stroke_widths[eidx]) - print >> f, ' d="M {0},{1} {2},{3}"'.format(x1, y1, x2, y2) - print >> f, ' id="path{0}"'.format(eidx) - print >> f, ' inkscape:connector-type="polyline"' - print >> f, ' inkscape:connector-curvature="0"' - print >> f, ' inkscape:connection-start="#g{0}"'.format(edge.source) - print >> f, ' inkscape:connection-start-point="d4"' - print >> f, ' inkscape:connection-end="#g{0}"'.format(edge.target) - print >> f, ' inkscape:connection-end-point="d4" />' - - print >> f, " " - print >> f - - print >> f, ' ' - print >> f, ' ' + print("', file=f) + + print(" ", file=f) + print(file=f) + + print( + ' ', + file=f, + ) + print(" ", file=f) if any(x == 3 for x in shapes): # Only import tkFont if we really need it. Unfortunately, this will # flash up an unneccesary Tk window in some cases - import tkFont - import Tkinter as tk + import tkinter.font + import tkinter as tk # This allows us to dynamically size the width of the nodes. # Unfortunately this works only with font sizes specified in pixels. @@ -2472,54 +2605,103 @@ def write_svg(self, fname, layout="auto", width=None, height=None, \ try: font_size_in_pixels = int(font_size) except: - raise ValueError("font sizes must be specified in pixels " - "when any of the nodes has shape=3 (i.e. " - "node size determined by text size)") + raise ValueError( + "font sizes must be specified in pixels " + "when any of the nodes has shape=3 (i.e. " + "node size determined by text size)" + ) tk_window = tk.Tk() - font = tkFont.Font(root=tk_window, font=("Sans", font_size_in_pixels, tkFont.NORMAL)) + font = tkinter.font.Font( + root=tk_window, font=("Sans", font_size_in_pixels, tkinter.font.NORMAL) + ) else: tk_window = None for vidx in range(self.vcount()): - print >> f, ' '.\ - format(vidx, layout[vidx][0], layout[vidx][1]) + print( + ' '.format( + vidx, layout[vidx][0], layout[vidx][1] + ), + file=f, + ) if shapes[vidx] == 1: # Undocumented feature: can handle two colors but only for circles c = str(colors[vidx]) if " " in c: c = c.split(" ") vs = str(vertex_size) - print >> f, ' '.format(vs, c[0]) - print >> f, ' '.format(vs, c[1]) - print >> f, ' '\ - .format(vs) + print( + ' '.format( + vs, c[0] + ), + file=f, + ) + print( + ' '.format( + vs, c[1] + ), + file=f, + ) + print( + ' '.format(vs), + file=f, + ) else: - print >> f, ' '.\ - format(str(vertex_size), str(colors[vidx])) + print( + ' '.format( + str(vertex_size), str(colors[vidx]) + ), + file=f, + ) elif shapes[vidx] == 2: - print >> f, ' '.\ - format(vertex_size, vertex_size * 2, vidx, colors[vidx]) + print( + ' '.format( + vertex_size, vertex_size * 2, vidx, colors[vidx] + ), + file=f, + ) elif shapes[vidx] == 3: - (vertex_width, vertex_height) = (font.measure(str(labels[vidx])) + 2, font.metrics("linespace") + 2) - print >> f, ' '.\ - format(vertex_width / 2., vertex_height / 2., vertex_width, vertex_height, vidx, colors[vidx]) - - print >> f, ' '.format(vertex_size / 2.,vidx, font_size) - print >> f, '{2}'.format(vertex_size / 2.,vidx, str(labels[vidx])) - print >> f, ' ' - - print >> f, '' - print >> f - print >> f, '' + (vertex_width, vertex_height) = ( + font.measure(str(labels[vidx])) + 2, + font.metrics("linespace") + 2, + ) + print( + ' '.format( + vertex_width / 2.0, + vertex_height / 2.0, + vertex_width, + vertex_height, + vidx, + colors[vidx], + ), + file=f, + ) + + print( + ' '.format( + vertex_size / 2.0, vidx, font_size + ), + file=f, + ) + print( + '{2}'.format( + vertex_size / 2.0, vidx, str(labels[vidx]) + ), + file=f, + ) + print(" ", file=f) + + print("", file=f) + print(file=f) + print("", file=f) if our_file: f.close() if tk_window: tk_window.destroy() - @classmethod def _identify_format(klass, filename): """_identify_format(filename) @@ -2537,10 +2719,11 @@ def _identify_format(klass, filename): @return: the format of the file as a string. """ import os.path + if hasattr(filename, "name") and hasattr(filename, "read"): # It is most likely a file try: - filename=filename.name + filename = filename.name except: return None @@ -2555,9 +2738,25 @@ def _identify_format(klass, filename): elif ext2 == ".graphml": return "graphmlz" - if ext in [".graphml", ".graphmlz", ".lgl", ".ncol", ".pajek", - ".gml", ".dimacs", ".edgelist", ".edges", ".edge", ".net", - ".pickle", ".picklez", ".dot", ".gw", ".lgr", ".dl"]: + if ext in [ + ".graphml", + ".graphmlz", + ".lgl", + ".ncol", + ".pajek", + ".gml", + ".dimacs", + ".edgelist", + ".edges", + ".edge", + ".net", + ".pickle", + ".picklez", + ".dot", + ".gw", + ".lgr", + ".dl", + ]: return ext[1:] if ext == ".txt" or ext == ".dat": @@ -2623,8 +2822,8 @@ def Read(klass, f, format=None, *args, **kwds): raise IOError("no reader method for file format: %s" % str(format)) reader = getattr(klass, reader) return reader(f, *args, **kwds) - Load = Read + Load = Read def write(self, f, format=None, *args, **kwds): """Unified writing function for graphs. @@ -2680,15 +2879,22 @@ def write(self, f, format=None, *args, **kwds): raise IOError("no writer method for file format: %s" % str(format)) writer = getattr(self, writer) return writer(f, *args, **kwds) + save = write ##################################################### # Constructor for dict-like representation of graphs @classmethod - def DictList(klass, vertices, edges, directed=False, \ - vertex_name_attr="name", edge_foreign_keys=("source", "target"), \ - iterative=False): + def DictList( + klass, + vertices, + edges, + directed=False, + vertex_name_attr="name", + edge_foreign_keys=("source", "target"), + iterative=False, + ): """Constructs a graph from a list-of-dictionaries representation. This representation assumes that vertices and edges are encoded in @@ -2721,6 +2927,7 @@ def DictList(klass, vertices, edges, directed=False, \ add the edges in a batch from an edge list. @return: the graph that was constructed """ + def create_list_from_indices(l, n): result = [None] * n for i, v in l: @@ -2731,13 +2938,13 @@ def create_list_from_indices(l, n): vertex_attrs, n = {}, 0 if vertices: for idx, vertex_data in enumerate(vertices): - for k, v in vertex_data.iteritems(): + for k, v in vertex_data.items(): try: vertex_attrs[k].append((idx, v)) except KeyError: vertex_attrs[k] = [(idx, v)] n += 1 - for k, v in vertex_attrs.iteritems(): + for k, v in vertex_attrs.items(): vertex_attrs[k] = create_list_from_indices(v, n) else: vertex_attrs[vertex_name_attr] = [] @@ -2747,7 +2954,7 @@ def create_list_from_indices(l, n): if len(vertex_names) != len(set(vertex_names)): raise ValueError("vertex names are not unique") # Create a reverse mapping from vertex names to indices - vertex_name_map = UniqueIdGenerator(initial = vertex_names) + vertex_name_map = UniqueIdGenerator(initial=vertex_names) # Construct the edges efk_src, efk_dest = edge_foreign_keys @@ -2766,7 +2973,7 @@ def create_list_from_indices(l, n): g.vs[n][vertex_name_attr] = dst_name n += 1 g.add_edge(v1, v2) - for k, v in edge_data.iteritems(): + for k, v in edge_data.items(): g.es[idx][k] = v return g @@ -2777,13 +2984,13 @@ def create_list_from_indices(l, n): v2 = vertex_name_map[edge_data[efk_dest]] edge_list.append((v1, v2)) - for k, v in edge_data.iteritems(): + for k, v in edge_data.items(): try: edge_attrs[k].append((idx, v)) except KeyError: edge_attrs[k] = [(idx, v)] m += 1 - for k, v in edge_attrs.iteritems(): + for k, v in edge_attrs.items(): edge_attrs[k] = create_list_from_indices(v, m) # It may have happened that some vertices were added during @@ -2791,8 +2998,9 @@ def create_list_from_indices(l, n): if len(vertex_name_map) > n: diff = len(vertex_name_map) - n more = [None] * diff - for k, v in vertex_attrs.iteritems(): v.extend(more) - vertex_attrs[vertex_name_attr] = vertex_name_map.values() + for k, v in vertex_attrs.items(): + v.extend(more) + vertex_attrs[vertex_name_attr] = list(vertex_name_map.values()) n = len(vertex_name_map) # Create the graph @@ -2802,8 +3010,14 @@ def create_list_from_indices(l, n): # Constructor for tuple-like representation of graphs @classmethod - def TupleList(klass, edges, directed=False, \ - vertex_name_attr="name", edge_attrs=None, weights=False): + def TupleList( + klass, + edges, + directed=False, + vertex_name_attr="name", + edge_attrs=None, + weights=False, + ): """Constructs a graph from a list-of-tuples representation. This representation assumes that the edges of the graph are encoded @@ -2851,15 +3065,16 @@ def TupleList(klass, edges, directed=False, \ if not weights: edge_attrs = () else: - if not isinstance(weights, basestring): + if not isinstance(weights, str): weights = "weight" edge_attrs = [weights] else: if weights: - raise ValueError("`weights` must be False if `edge_attrs` is " - "not None") + raise ValueError( + "`weights` must be False if `edge_attrs` is " "not None" + ) - if isinstance(edge_attrs, basestring): + if isinstance(edge_attrs, str): edge_attrs = [edge_attrs] # Set up a vertex ID generator @@ -2881,7 +3096,7 @@ def TupleList(klass, edges, directed=False, \ # Set up the "name" vertex attribute vertex_attributes = {} - vertex_attributes[vertex_name_attr] = idgen.values() + vertex_attributes[vertex_name_attr] = list(idgen.values()) n = len(idgen) # Construct the graph @@ -2889,7 +3104,7 @@ def TupleList(klass, edges, directed=False, \ ################################# # Constructor for graph formulae - Formula=classmethod(construct_graph_from_formula) + Formula = classmethod(construct_graph_from_formula) ########################### # Vertex and edge sequence @@ -3109,45 +3324,50 @@ def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): raise ValueError("the data frame should contain at least two columns") if use_vids: - if str(edges.dtypes[0]).startswith('int') and \ - str(edges.dtypes[1]).startswith('int'): + if str(edges.dtypes[0]).startswith("int") and str( + edges.dtypes[1] + ).startswith("int"): names_edges = None else: - raise TypeError('vertex ids must be 0-based integers') + raise TypeError("vertex ids must be 0-based integers") else: # Handle if some elements are 'NA' if edges.iloc[:, :2].isna().values.any(): warn("In 'edges' NA elements were replaced with string \"NA\"") edges = edges.copy() - edges.iloc[:, :2].fillna('NA', inplace=True) + edges.iloc[:, :2].fillna("NA", inplace=True) names_edges = np.unique(edges.values[:, :2]) if (vertices is not None) and vertices.iloc[:, 0].isna().values.any(): - warn("In the first column of 'vertices' NA elements were replaced "+ - "with string \"NA\"") + warn( + "In the first column of 'vertices' NA elements were replaced " + + 'with string "NA"' + ) vertices = vertices.copy() - vertices.iloc[:, 0].fillna('NA', inplace=True) + vertices.iloc[:, 0].fillna("NA", inplace=True) if vertices is None: names = names_edges else: if vertices.shape[1] < 1: - raise ValueError('vertices has no columns') + raise ValueError("vertices has no columns") names_vertices = vertices.iloc[:, 0] if names_vertices.duplicated().any(): - raise ValueError('Vertex names must be unique') + raise ValueError("Vertex names must be unique") names_vertices = names_vertices.values - if (names_edges is not None) and \ - len(np.setdiff1d(names_edges, names_vertices)): + if (names_edges is not None) and len( + np.setdiff1d(names_edges, names_vertices) + ): raise ValueError( - 'Some vertices in the edge DataFrame are missing from '+ - 'vertices DataFrame') + "Some vertices in the edge DataFrame are missing from " + + "vertices DataFrame" + ) names = names_vertices @@ -3161,7 +3381,7 @@ def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): # vertex names if names is not None: for v, name in zip(g.vs, names): - v['name'] = name + v["name"] = name # vertex attributes if (vertices is not None) and (vertices.shape[1] > 1): @@ -3180,18 +3400,19 @@ def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): e1 = edges.values[:, 1] # add the edges - g.add_edges(zip(e0, e1)) + g.add_edges(list(zip(e0, e1))) # edge attributes if edges.shape[1] > 2: for e, (_, attr) in zip(g.es, edges.iloc[:, 2:].iterrows()): - for a_name, a_value in attr.items(): + for a_name, a_value in list(attr.items()): e[a_name] = a_value return g - def bipartite_projection(self, types="type", multiplicity=True, probe1=-1, - which="both"): + def bipartite_projection( + self, types="type", multiplicity=True, probe1=-1, which="both" + ): """Projects a bipartite graph into two one-mode graphs. Edge directions are ignored while projecting. @@ -3276,8 +3497,7 @@ def bipartite_projection_size(self, types="type", *args, **kwds): first projection, followed by the number of vertices and edges in the second projection. """ - return super(Graph, self).bipartite_projection_size(types, \ - *args, **kwds) + return super(Graph, self).bipartite_projection_size(types, *args, **kwds) def get_incidence(self, types="type", *args, **kwds): """get_incidence(self, types="type") @@ -3355,7 +3575,7 @@ def __iadd__(self, other): @see: L{__add__} """ - if isinstance(other, (int, basestring)): + if isinstance(other, (int, str)): self.add_vertices(other) return self elif isinstance(other, tuple) and len(other) == 2: @@ -3367,12 +3587,11 @@ def __iadd__(self, other): if isinstance(other[0], tuple): self.add_edges(other) return self - if isinstance(other[0], basestring): + if isinstance(other[0], str): self.add_vertices(other) return self return NotImplemented - def __add__(self, other): """Copies the graph and extends the copy depending on the type of the other object given. @@ -3385,18 +3604,18 @@ def __add__(self, other): is extended by multiple edges. If it is a L{Graph}, a disjoint union is performed. """ - if isinstance(other, (int, basestring)): + if isinstance(other, (int, str)): g = self.copy() g.add_vertices(other) elif isinstance(other, tuple) and len(other) == 2: g = self.copy() g.add_edges([other]) elif isinstance(other, list): - if len(other)>0: + if len(other) > 0: if isinstance(other[0], tuple): g = self.copy() g.add_edges(other) - elif isinstance(other[0], basestring): + elif isinstance(other[0], str): g = self.copy() g.add_vertices(other) elif isinstance(other[0], Graph): @@ -3413,7 +3632,6 @@ def __add__(self, other): return g - def __and__(self, other): """Graph intersection operator. @@ -3425,7 +3643,6 @@ def __and__(self, other): else: return NotImplemented - def __isub__(self, other): """In-place subtraction (difference). @@ -3438,7 +3655,7 @@ def __isub__(self, other): if len(other) > 0: if isinstance(other[0], tuple): self.delete_edges(other) - elif isinstance(other[0], (int, long, basestring)): + elif isinstance(other[0], (int, str)): self.delete_vertices(other) else: return NotImplemented @@ -3454,7 +3671,6 @@ def __isub__(self, other): return NotImplemented return self - def __sub__(self, other): """Removes the given object(s) from the graph @@ -3469,7 +3685,7 @@ def __sub__(self, other): return self.difference(other) result = self.copy() - if isinstance(other, (int, long, basestring)): + if isinstance(other, (int, str)): result.delete_vertices([other]) elif isinstance(other, tuple) and len(other) == 2: result.delete_edges([other]) @@ -3477,7 +3693,7 @@ def __sub__(self, other): if len(other) > 0: if isinstance(other[0], tuple): result.delete_edges(other) - elif isinstance(other[0], (int, long, basestring)): + elif isinstance(other[0], (int, str)): result.delete_vertices(other) else: return NotImplemented @@ -3516,9 +3732,8 @@ def __mul__(self, other): return NotImplemented - def __nonzero__(self): - """Returns True if the graph has at least one vertex, False otherwise. - """ + def __bool__(self): + """Returns True if the graph has at least one vertex, False otherwise.""" return self.vcount() > 0 def __or__(self, other): @@ -3532,14 +3747,13 @@ def __or__(self, other): else: return NotImplemented - def __coerce__(self, other): """Coercion rules. This method is needed to allow the graph to react to additions with lists, tuples, integers, strings, vertices, edges and so on. """ - if isinstance(other, (int, tuple, list, basestring)): + if isinstance(other, (int, tuple, list, str)): return self, other if isinstance(other, _igraph.Vertex): return self, other @@ -3571,12 +3785,18 @@ def __reduce__(self): vattrs[attr] = self.vs[attr] for attr in self.es.attribute_names(): eattrs[attr] = self.es[attr] - parameters = (self.vcount(), self.get_edgelist(), \ - self.is_directed(), gattrs, vattrs, eattrs) + parameters = ( + self.vcount(), + self.get_edgelist(), + self.is_directed(), + gattrs, + vattrs, + eattrs, + ) return (constructor, parameters, self.__dict__) - __iter__ = None # needed for PyPy - __hash__ = None # needed for PyPy + __iter__ = None # needed for PyPy + __hash__ = None # needed for PyPy def __plot__(self, context, bbox, palette, *args, **kwds): """Plots the graph to the given Cairo context in the given bounding box @@ -3780,11 +4000,11 @@ def __str__(self): output. """ params = dict( - verbosity=1, - width=78, - print_graph_attributes=False, - print_vertex_attributes=False, - print_edge_attributes=False + verbosity=1, + width=78, + print_graph_attributes=False, + print_vertex_attributes=False, + print_edge_attributes=False, ) return self.summary(**params) @@ -3820,7 +4040,7 @@ def disjoint_union(self, other): other = [other] return disjoint_union([self] + other) - def union(self, other, byname='auto'): + def union(self, other, byname="auto"): """union(self, other, byname="auto") Creates the union of two (or more) graphs. @@ -3834,7 +4054,7 @@ def union(self, other, byname='auto'): other = [other] return union([self] + other, byname=byname) - def intersection(self, other, byname='auto'): + def intersection(self, other, byname="auto"): """intersection(self, other, byname="auto") Creates the intersection of two (or more) graphs. @@ -3850,29 +4070,29 @@ def intersection(self, other, byname='auto'): return intersection([self] + other, byname=byname) _format_mapping = { - "ncol": ("Read_Ncol", "write_ncol"), - "lgl": ("Read_Lgl", "write_lgl"), - "graphdb": ("Read_GraphDB", None), - "graphmlz": ("Read_GraphMLz", "write_graphmlz"), - "graphml": ("Read_GraphML", "write_graphml"), - "gml": ("Read_GML", "write_gml"), - "dot": (None, "write_dot"), - "graphviz": (None, "write_dot"), - "net": ("Read_Pajek", "write_pajek"), - "pajek": ("Read_Pajek", "write_pajek"), - "dimacs": ("Read_DIMACS", "write_dimacs"), - "adjacency": ("Read_Adjacency", "write_adjacency"), - "adj": ("Read_Adjacency", "write_adjacency"), - "edgelist": ("Read_Edgelist", "write_edgelist"), - "edge": ("Read_Edgelist", "write_edgelist"), - "edges": ("Read_Edgelist", "write_edgelist"), - "pickle": ("Read_Pickle", "write_pickle"), - "picklez": ("Read_Picklez", "write_picklez"), - "svg": (None, "write_svg"), - "gw": (None, "write_leda"), - "leda": (None, "write_leda"), - "lgr": (None, "write_leda"), - "dl": ("Read_DL", None) + "ncol": ("Read_Ncol", "write_ncol"), + "lgl": ("Read_Lgl", "write_lgl"), + "graphdb": ("Read_GraphDB", None), + "graphmlz": ("Read_GraphMLz", "write_graphmlz"), + "graphml": ("Read_GraphML", "write_graphml"), + "gml": ("Read_GML", "write_gml"), + "dot": (None, "write_dot"), + "graphviz": (None, "write_dot"), + "net": ("Read_Pajek", "write_pajek"), + "pajek": ("Read_Pajek", "write_pajek"), + "dimacs": ("Read_DIMACS", "write_dimacs"), + "adjacency": ("Read_Adjacency", "write_adjacency"), + "adj": ("Read_Adjacency", "write_adjacency"), + "edgelist": ("Read_Edgelist", "write_edgelist"), + "edge": ("Read_Edgelist", "write_edgelist"), + "edges": ("Read_Edgelist", "write_edgelist"), + "pickle": ("Read_Pickle", "write_pickle"), + "picklez": ("Read_Picklez", "write_picklez"), + "svg": (None, "write_svg"), + "gw": (None, "write_leda"), + "leda": (None, "write_leda"), + "lgr": (None, "write_leda"), + "dl": ("Read_DL", None), } _layout_mapping = { @@ -3912,8 +4132,10 @@ def intersection(self, other, byname='auto'): # After adjusting something here, don't forget to update the docstring # of Graph.layout if necessary! + ############################################################## + class VertexSeq(_igraph.VertexSeq): """Class representing a sequence of vertices in the graph. @@ -4014,7 +4236,7 @@ def find(self, *args, **kwds): else: name = None - if name is not None and isinstance(name, (str, unicode)): + if name is not None and isinstance(name, str): args = [name] if args: @@ -4139,15 +4361,16 @@ def select(self, *args, **kwds): vs = _igraph.VertexSeq.select(self, *args) operators = { - "lt": operator.lt, \ - "gt": operator.gt, \ - "le": operator.le, \ - "ge": operator.ge, \ - "eq": operator.eq, \ - "ne": operator.ne, \ - "in": lambda a, b: a in b, \ - "notin": lambda a, b: a not in b } - for keyword, value in kwds.iteritems(): + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "eq": operator.eq, + "ne": operator.ne, + "in": lambda a, b: a in b, + "notin": lambda a, b: a not in b, + } + for keyword, value in kwds.items(): if "_" not in keyword or keyword.rindex("_") == 0: keyword = keyword + "_eq" attr, _, op = keyword.rpartition("_") @@ -4157,12 +4380,12 @@ def select(self, *args, **kwds): # No such operator, assume that it's part of the attribute name attr, op, func = keyword, "eq", operators["eq"] - if attr[0] == '_': + if attr[0] == "_": # Method call, not an attribute values = getattr(vs.graph, attr[1:])(vs) else: values = vs[attr] - filtered_idxs=[i for i, v in enumerate(values) if func(v, value)] + filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] vs = vs.select(filtered_idxs) return vs @@ -4174,8 +4397,10 @@ def __call__(self, *args, **kwds): """ return self.select(*args, **kwds) + ############################################################## + class EdgeSeq(_igraph.EdgeSeq): """Class representing a sequence of edges in the graph. @@ -4433,14 +4658,15 @@ def _ensure_set(value): return value operators = { - "lt": operator.lt, \ - "gt": operator.gt, \ - "le": operator.le, \ - "ge": operator.ge, \ - "eq": operator.eq, \ - "ne": operator.ne, \ - "in": lambda a, b: a in b, \ - "notin": lambda a, b: a not in b } + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "eq": operator.eq, + "ne": operator.ne, + "in": lambda a, b: a in b, + "notin": lambda a, b: a not in b, + } # TODO(ntamas): some keyword arguments should be prioritized over # others; for instance, we have optimized code paths for _source and @@ -4448,18 +4674,18 @@ def _ensure_set(value): # these should be executed first. This matters only if there are # multiple keyword arguments and es.is_all() is True. - for keyword, value in kwds.iteritems(): + for keyword, value in kwds.items(): if "_" not in keyword or keyword.rindex("_") == 0: keyword = keyword + "_eq" pos = keyword.rindex("_") - attr, op = keyword[0:pos], keyword[pos+1:] + attr, op = keyword[0:pos], keyword[pos + 1 :] try: func = operators[op] except KeyError: # No such operator, assume that it's part of the attribute name attr, op, func = keyword, "eq", operators["eq"] - if attr[0] == '_': + if attr[0] == "_": if attr in ("_source", "_from", "_target", "_to") and not is_directed: if op not in ("eq", "in"): raise RuntimeError("unsupported for undirected graphs") @@ -4467,7 +4693,7 @@ def _ensure_set(value): # translate to _incident to avoid confusion attr = "_incident" if func == operators["eq"]: - if hasattr(value, "__iter__") and not isinstance(value, (str, unicode)): + if hasattr(value, "__iter__") and not isinstance(value, str): value = set(value) else: value = set([value]) @@ -4497,7 +4723,7 @@ def _ensure_set(value): value = _ensure_set(value) elif attr == "_incident": - func = None # ignoring function, filtering here + func = None # ignoring function, filtering here value = _ensure_set(value) # Fetch all the edges that are incident on at least one of @@ -4508,13 +4734,15 @@ def _ensure_set(value): if not es.is_all(): # Find those that are in the current edge sequence - filtered_idxs = [i for i, e in enumerate(es) if e.index in candidates] + filtered_idxs = [ + i for i, e in enumerate(es) if e.index in candidates + ] else: # We are done, the filtered indexes are in the candidates set filtered_idxs = sorted(candidates) elif attr == "_within": - func = None # ignoring function, filtering here + func = None # ignoring function, filtering here value = _ensure_set(value) # Fetch all the edges that are incident on at least one of @@ -4525,18 +4753,28 @@ def _ensure_set(value): if not es.is_all(): # Find those where both endpoints are OK - filtered_idxs = [i for i, e in enumerate(es) if e.index in candidates - and e.source in value and e.target in value] + filtered_idxs = [ + i + for i, e in enumerate(es) + if e.index in candidates + and e.source in value + and e.target in value + ] else: # Optimized version when the edge sequence contains all the edges # exactly once in increasing order of edge IDs - filtered_idxs = [i for i in candidates - if es[i].source in value and es[i].target in value] + filtered_idxs = [ + i + for i in candidates + if es[i].source in value and es[i].target in value + ] elif attr == "_between": if len(value) != 2: - raise ValueError("_between selector requires two vertex ID lists") - func = None # ignoring function, filtering here + raise ValueError( + "_between selector requires two vertex ID lists" + ) + func = None # ignoring function, filtering here set1 = _ensure_set(value[0]) set2 = _ensure_set(value[1]) @@ -4550,15 +4788,21 @@ def _ensure_set(value): if not es.is_all(): # Find those where both endpoints are OK - filtered_idxs = [i for i, e in enumerate(es) - if (e.source in set1 and e.target in set2) or - (e.target in set1 and e.source in set2)] + filtered_idxs = [ + i + for i, e in enumerate(es) + if (e.source in set1 and e.target in set2) + or (e.target in set1 and e.source in set2) + ] else: # Optimized version when the edge sequence contains all the edges # exactly once in increasing order of edge IDs - filtered_idxs = [i for i in candidates - if (es[i].source in set1 and es[i].target in set2) or - (es[i].target in set1 and es[i].source in set2)] + filtered_idxs = [ + i + for i in candidates + if (es[i].source in set1 and es[i].target in set2) + or (es[i].target in set1 and es[i].source in set2) + ] else: # Method call, not an attribute @@ -4575,7 +4819,6 @@ def _ensure_set(value): return es - def __call__(self, *args, **kwds): """Shorthand notation to select() @@ -4583,9 +4826,11 @@ def __call__(self, *args, **kwds): """ return self.select(*args, **kwds) + ############################################################## # Additional methods of VertexSeq and EdgeSeq that call Graph methods + def _graphmethod(func=None, name=None): """Auxiliary decorator @@ -4604,10 +4849,13 @@ def _graphmethod(func=None, name=None): method = getattr(Graph, name) if hasattr(func, "__call__"): + def decorated(*args, **kwds): self = args[0].graph return func(args[0], method(self, *args, **kwds)) + else: + def decorated(*args, **kwds): self = args[0].graph return method(self, *args, **kwds) @@ -4619,10 +4867,13 @@ def decorated(*args, **kwds): restricted to this sequence, and returns the result. @see: Graph.%(name)s() for details. -""" % { "name": name } +""" % { + "name": name + } return decorated + def _add_proxy_methods(): # Proxy methods for VertexSeq and EdgeSeq that forward their arguments to @@ -4631,38 +4882,63 @@ def _add_proxy_methods(): # the C source whenever you add a proxy method here if that makes sense for # an individual vertex or edge decorated_methods = {} - decorated_methods[VertexSeq] = \ - ["degree", "betweenness", "bibcoupling", "closeness", "cocitation", - "constraint", "diversity", "eccentricity", "get_shortest_paths", "maxdegree", - "pagerank", "personalized_pagerank", "shortest_paths", "similarity_dice", - "similarity_jaccard", "subgraph", "indegree", "outdegree", "isoclass", - "delete_vertices", "is_separator", "is_minimal_separator"] - decorated_methods[EdgeSeq] = \ - ["count_multiple", "delete_edges", "is_loop", "is_multiple", - "is_mutual", "subgraph_edges"] + decorated_methods[VertexSeq] = [ + "degree", + "betweenness", + "bibcoupling", + "closeness", + "cocitation", + "constraint", + "diversity", + "eccentricity", + "get_shortest_paths", + "maxdegree", + "pagerank", + "personalized_pagerank", + "shortest_paths", + "similarity_dice", + "similarity_jaccard", + "subgraph", + "indegree", + "outdegree", + "isoclass", + "delete_vertices", + "is_separator", + "is_minimal_separator", + ] + decorated_methods[EdgeSeq] = [ + "count_multiple", + "delete_edges", + "is_loop", + "is_multiple", + "is_mutual", + "subgraph_edges", + ] rename_methods = {} - rename_methods[VertexSeq] = { - "delete_vertices": "delete" - } - rename_methods[EdgeSeq] = { - "delete_edges": "delete", - "subgraph_edges": "subgraph" - } + rename_methods[VertexSeq] = {"delete_vertices": "delete"} + rename_methods[EdgeSeq] = {"delete_edges": "delete", "subgraph_edges": "subgraph"} - for klass, methods in decorated_methods.iteritems(): + for klass, methods in decorated_methods.items(): for method in methods: new_method_name = rename_methods[klass].get(method, method) setattr(klass, new_method_name, _graphmethod(None, method)) - setattr(EdgeSeq, "edge_betweenness", _graphmethod( \ - lambda self, result: [result[i] for i in self.indices], "edge_betweenness")) + setattr( + EdgeSeq, + "edge_betweenness", + _graphmethod( + lambda self, result: [result[i] for i in self.indices], "edge_betweenness" + ), + ) + _add_proxy_methods() ############################################################## # Making sure that layout methods always return a Layout + def _layout_method_wrapper(func): """Wraps an existing layout method to ensure that it returns a Layout instead of a list of lists. @@ -4670,15 +4946,18 @@ def _layout_method_wrapper(func): @param func: the method to wrap. Must be a method of the Graph object. @return: a new method """ + def result(*args, **kwds): layout = func(*args, **kwds) if not isinstance(layout, Layout): layout = Layout(layout) return layout + result.__name__ = func.__name__ - result.__doc__ = func.__doc__ + result.__doc__ = func.__doc__ return result + for name in dir(Graph): if not name.startswith("layout_"): continue @@ -4689,6 +4968,7 @@ def result(*args, **kwds): ############################################################## # Adding aliases for the 3D versions of the layout methods + def _3d_version_for(func): """Creates an alias for the 3D version of the given layout algoritm. @@ -4698,22 +4978,30 @@ def _3d_version_for(func): @param func: must be a method of the Graph object. @return: a new method """ + def result(*args, **kwds): kwds["dim"] = 3 return func(*args, **kwds) + result.__name__ = "%s_3d" % func.__name__ - result.__doc__ = """Alias for L{%s()} with dim=3.\n\n@see: Graph.%s()""" \ - % (func.__name__, func.__name__) + result.__doc__ = """Alias for L{%s()} with dim=3.\n\n@see: Graph.%s()""" % ( + func.__name__, + func.__name__, + ) return result -Graph.layout_fruchterman_reingold_3d=_3d_version_for(Graph.layout_fruchterman_reingold) -Graph.layout_kamada_kawai_3d=_3d_version_for(Graph.layout_kamada_kawai) -Graph.layout_random_3d=_3d_version_for(Graph.layout_random) -Graph.layout_grid_3d=_3d_version_for(Graph.layout_grid) -Graph.layout_sphere=_3d_version_for(Graph.layout_circle) + +Graph.layout_fruchterman_reingold_3d = _3d_version_for( + Graph.layout_fruchterman_reingold +) +Graph.layout_kamada_kawai_3d = _3d_version_for(Graph.layout_kamada_kawai) +Graph.layout_random_3d = _3d_version_for(Graph.layout_random) +Graph.layout_grid_3d = _3d_version_for(Graph.layout_grid) +Graph.layout_sphere = _3d_version_for(Graph.layout_circle) ############################################################## + def autocurve(graph, attribute="curved", default=0): """Calculates curvature values for each of the edges in the graph to make sure that multiple edges are shown properly on a graph plot. @@ -4749,7 +5037,7 @@ def autocurve(graph, attribute="curved", default=0): multiplicities[u, v].append(edge.index) result = [default] * graph.ecount() - for pair, eids in multiplicities.iteritems(): + for pair, eids in multiplicities.items(): # Is it a single edge? if len(eids) < 2: continue @@ -4764,9 +5052,9 @@ def autocurve(graph, attribute="curved", default=0): for idx, eid in enumerate(eids): edge = graph.es[eid] if edge.source > edge.target: - result[eid] = -sign*curve + result[eid] = -sign * curve else: - result[eid] = sign*curve + result[eid] = sign * curve if idx % 2 == 1: curve += dcurve sign *= -1 @@ -4781,14 +5069,18 @@ def get_include(): """Returns the folder that contains the C API headers of the Python interface of igraph.""" import igraph + paths = [ # The following path works if python-igraph is installed already - os.path.join(sys.prefix, "include", - "python{0}.{1}".format(*sys.version_info), - "python-igraph"), + os.path.join( + sys.prefix, + "include", + "python{0}.{1}".format(*sys.version_info), + "python-igraph", + ), # Fallback for cases when python-igraph is not installed but # imported directly from the source tree - os.path.join(os.path.dirname(igraph.__file__), "..", "src") + os.path.join(os.path.dirname(igraph.__file__), "..", "src"), ] for path in paths: if os.path.exists(os.path.join(path, "igraphmodule_api.h")): @@ -4805,7 +5097,10 @@ def read(filename, *args, **kwds): @param filename: the name of the file to be loaded """ return Graph.Read(filename, *args, **kwds) -load=read + + +load = read + def write(graph, filename, *args, **kwds): """Saves a graph to the given file. @@ -4817,7 +5112,10 @@ def write(graph, filename, *args, **kwds): @param filename: the name of the file to be written """ return graph.write(filename, *args, **kwds) -save=write + + +save = write + def summary(obj, stream=None, *args, **kwds): """Prints a summary of object o to a given stream @@ -4837,5 +5135,6 @@ def summary(obj, stream=None, *args, **kwds): stream.write(str(obj)) stream.write("\n") + config = configuration.init() del construct_graph_from_formula diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index c58f014fa..9b9293055 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -19,7 +19,6 @@ Mac OS X users are likely to invoke igraph from the command line. """ -from __future__ import print_function import re import sys @@ -72,40 +71,41 @@ class TerminalController: @author: Edward Loper """ + # Cursor movement: - BOL = '' #: Move the cursor to the beginning of the line - UP = '' #: Move the cursor up one line - DOWN = '' #: Move the cursor down one line - LEFT = '' #: Move the cursor left one char - RIGHT = '' #: Move the cursor right one char + BOL = "" #: Move the cursor to the beginning of the line + UP = "" #: Move the cursor up one line + DOWN = "" #: Move the cursor down one line + LEFT = "" #: Move the cursor left one char + RIGHT = "" #: Move the cursor right one char # Deletion: - CLEAR_SCREEN = '' #: Clear the screen and move to home position - CLEAR_EOL = '' #: Clear to the end of the line. - CLEAR_BOL = '' #: Clear to the beginning of the line. - CLEAR_EOS = '' #: Clear to the end of the screen + CLEAR_SCREEN = "" #: Clear the screen and move to home position + CLEAR_EOL = "" #: Clear to the end of the line. + CLEAR_BOL = "" #: Clear to the beginning of the line. + CLEAR_EOS = "" #: Clear to the end of the screen # Output modes: - BOLD = '' #: Turn on bold mode - BLINK = '' #: Turn on blink mode - DIM = '' #: Turn on half-bright mode - REVERSE = '' #: Turn on reverse-video mode - NORMAL = '' #: Turn off all modes + BOLD = "" #: Turn on bold mode + BLINK = "" #: Turn on blink mode + DIM = "" #: Turn on half-bright mode + REVERSE = "" #: Turn on reverse-video mode + NORMAL = "" #: Turn off all modes # Cursor display: - HIDE_CURSOR = '' #: Make the cursor invisible - SHOW_CURSOR = '' #: Make the cursor visible + HIDE_CURSOR = "" #: Make the cursor invisible + SHOW_CURSOR = "" #: Make the cursor visible # Terminal size: - COLS = None #: Width of the terminal (None for unknown) - LINES = None #: Height of the terminal (None for unknown) + COLS = None #: Width of the terminal (None for unknown) + LINES = None #: Height of the terminal (None for unknown) # Foreground colors: - BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' + BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = "" # Background colors: - BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = '' - BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = '' + BG_BLACK = BG_BLUE = BG_GREEN = BG_CYAN = "" + BG_RED = BG_MAGENTA = BG_YELLOW = BG_WHITE = "" _STRING_CAPABILITIES = """ BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 @@ -137,35 +137,35 @@ def __init__(self, term_stream=sys.stdout): # terminal has no capabilities. try: curses.setupterm() - except StandardError: + except Exception: return # Look up numeric capabilities. - self.COLS = curses.tigetnum('cols') - self.LINES = curses.tigetnum('lines') + self.COLS = curses.tigetnum("cols") + self.LINES = curses.tigetnum("lines") # Look up string capabilities. for capability in self._STRING_CAPABILITIES: - (attrib, cap_name) = capability.split('=') - setattr(self, attrib, self._tigetstr(cap_name) or '') + (attrib, cap_name) = capability.split("=") + setattr(self, attrib, self._tigetstr(cap_name) or "") # Colors - set_fg = self._tigetstr('setf') + set_fg = self._tigetstr("setf") if set_fg: for i, color in zip(range(len(self._COLORS)), self._COLORS): - setattr(self, color, self._tparm(set_fg, i) or '') - set_fg_ansi = self._tigetstr('setaf') + setattr(self, color, self._tparm(set_fg, i) or "") + set_fg_ansi = self._tigetstr("setaf") if set_fg_ansi: for i, color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): - setattr(self, color, self._tparm(set_fg_ansi, i) or '') - set_bg = self._tigetstr('setb') + setattr(self, color, self._tparm(set_fg_ansi, i) or "") + set_bg = self._tigetstr("setb") if set_bg: for i, color in zip(range(len(self._COLORS)), self._COLORS): - setattr(self, 'BG_'+color, self._tparm(set_bg, i) or '') - set_bg_ansi = self._tigetstr('setab') + setattr(self, "BG_" + color, self._tparm(set_bg, i) or "") + set_bg_ansi = self._tigetstr("setab") if set_bg_ansi: for i, color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): - setattr(self, 'BG_'+color, self._tparm(set_bg_ansi, i) or '') + setattr(self, "BG_" + color, self._tparm(set_bg_ansi, i) or "") @staticmethod def _tigetstr(cap_name): @@ -175,14 +175,16 @@ def _tigetstr(cap_name): # For any modern terminal, we should be able to just ignore # these, so strip them out. import curses - cap = curses.tigetstr(cap_name) or b'' + + cap = curses.tigetstr(cap_name) or b"" cap = cap.decode("latin-1") - return re.sub(r'\$<\d+>[/*]?', '', cap) + return re.sub(r"\$<\d+>[/*]?", "", cap) @staticmethod def _tparm(cap_name, param): import curses - cap = curses.tparm(cap_name.encode("latin-1"), param) or b'' + + cap = curses.tparm(cap_name.encode("latin-1"), param) or b"" return cap.decode("latin-1") def render(self, template): @@ -191,12 +193,12 @@ def render(self, template): the corresponding terminal control string (if it's defined) or '' (if it's not). """ - return re.sub('r\$\$|\${\w+}', self._render_sub, template) + return re.sub("r\$\$|\${\w+}", self._render_sub, template) def _render_sub(self, match): """Helper function for L{render}""" s = match.group() - if s == '$$': + if s == "$$": return s else: return getattr(self, s[2:-1]) @@ -212,18 +214,21 @@ class ProgressBar: The progress bar is colored, if the terminal supports color output; and adjusts to the width of the terminal. """ - BAR = '%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}' - HEADER = '${BOLD}${CYAN}%s${NORMAL}\n' + + BAR = "%3d%% ${GREEN}[${BOLD}%s%s${NORMAL}${GREEN}]${NORMAL}" + HEADER = "${BOLD}${CYAN}%s${NORMAL}\n" def __init__(self, term): self.term = term if not (self.term.CLEAR_EOL and self.term.UP and self.term.BOL): - raise ValueError("Terminal isn't capable enough -- you " - "should use a simpler progress display.") + raise ValueError( + "Terminal isn't capable enough -- you " + "should use a simpler progress display." + ) self.width = self.term.COLS or 75 self.progress_bar = term.render(self.BAR) self.header = self.term.render(self.HEADER % "".center(self.width)) - self.cleared = True #: true if we haven't drawn the bar yet. + self.cleared = True #: true if we haven't drawn the bar yet. self.last_percent = 0 self.last_message = "" @@ -237,7 +242,7 @@ def update(self, percent=None, message=None): C{None}, the previous message will be used. """ if self.cleared: - sys.stdout.write("\n"+self.header) + sys.stdout.write("\n" + self.header) self.cleared = False if message is None: @@ -250,12 +255,16 @@ def update(self, percent=None, message=None): else: self.last_percent = percent - n = int((self.width-10)*(percent/100.0)) + n = int((self.width - 10) * (percent / 100.0)) sys.stdout.write( - self.term.BOL + self.term.UP + self.term.UP + self.term.CLEAR_EOL + - self.term.render(self.HEADER % message.center(self.width)) + - (self.progress_bar % (percent, '='*n, '-'*(self.width-10-n))) + "\n" - ) + self.term.BOL + + self.term.UP + + self.term.UP + + self.term.CLEAR_EOL + + self.term.render(self.HEADER % message.center(self.width)) + + (self.progress_bar % (percent, "=" * n, "-" * (self.width - 10 - n))) + + "\n" + ) def update_message(self, message): """Updates the message of the progress bar. @@ -267,9 +276,14 @@ def update_message(self, message): def clear(self): """Clears the progress bar (i.e. removes it from the screen)""" if not self.cleared: - sys.stdout.write(self.term.BOL + self.term.CLEAR_EOL + - self.term.UP + self.term.CLEAR_EOL + - self.term.UP + self.term.CLEAR_EOL) + sys.stdout.write( + self.term.BOL + + self.term.CLEAR_EOL + + self.term.UP + + self.term.CLEAR_EOL + + self.term.UP + + self.term.CLEAR_EOL + ) self.cleared = True self.last_percent = 0 self.last_message = "" @@ -336,7 +350,7 @@ def __init__(self): try: sys.ps1 except AttributeError: - sys.ps1 = '>>> ' + sys.ps1 = ">>> " root = idlelib.PyShell.Tk(className="Idle") idlelib.PyShell.fixwordbreaks(root) @@ -421,6 +435,7 @@ def __init__(self): import sys from IPython import __version__ as ipython_version + self.ipython_version = ipython_version try: @@ -434,6 +449,7 @@ def __init__(self): except ImportError: # IPython 0.10 and earlier import IPython.Shell + self._shell = IPython.Shell.start() self._shell.IP.runsource("from igraph import *") sys.argv.append("-nosep") @@ -466,6 +482,7 @@ def __call__(self): """Starts the embedded shell.""" if self._shell is None: from code import InteractiveConsole + self._shell = InteractiveConsole() print("igraph %s running inside " % __version__, end="", file=sys.stderr) self._shell.runsource("from igraph import *") @@ -483,11 +500,16 @@ def main(): else: print("No configuration file, using defaults", file=sys.stderr) - if config.has_key("shells"): + if "shells" in config: parts = [part.strip() for part in config["shells"].split(",")] shell_classes = [] - available_classes = dict([(k, v) for k, v in globals().iteritems() - if isinstance(v, type) and issubclass(v, Shell)]) + available_classes = dict( + [ + (k, v) + for k, v in globals().items() + if isinstance(v, type) and issubclass(v, Shell) + ] + ) for part in parts: klass = available_classes.get(part, None) if klass is None: @@ -497,6 +519,7 @@ def main(): else: shell_classes = [IPythonShell, ClassicPythonShell] import platform + if platform.system() == "Windows": shell_classes.insert(0, IDLEShell) @@ -507,7 +530,7 @@ def main(): try: shell = shell_class() break - except StandardError: + except Exception: # Try the next one if "Classic" in str(shell_class): raise @@ -524,5 +547,6 @@ def main(): print("No suitable Python shell was found.", file=sys.stderr) print("Check configuration variable `general.shells'.", file=sys.stderr) -if __name__ == '__main__': + +if __name__ == "__main__": sys.exit(main()) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 8f519ddb1..43dfee021 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -25,12 +25,10 @@ """ from copy import deepcopy -from itertools import izip from math import pi -from cStringIO import StringIO +from io import StringIO from igraph import community_to_membership -from igraph.compat import property from igraph.configuration import Configuration from igraph.datatypes import UniqueIdGenerator from igraph.drawing.colors import ClusterColoringPalette @@ -38,6 +36,7 @@ from igraph.summary import _get_wrapper_for_width from igraph.utils import str_to_orientation + class Clustering(object): """Class representing a clustering of an arbitrary ordered set. @@ -80,7 +79,7 @@ class Clustering(object): @undocumented: _formatted_cluster_iterator """ - def __init__(self, membership, params = None): + def __init__(self, membership, params=None): """Constructor. @param membership: the membership list -- that is, the cluster @@ -88,8 +87,8 @@ def __init__(self, membership, params = None): @param params: additional parameters to be stored in this object's dictionary.""" self._membership = list(membership) - if len(self._membership)>0: - self._len = max(m for m in self._membership if m is not None)+1 + if len(self._membership) > 0: + self._len = max(m for m in self._membership if m is not None) + 1 else: self._len = 0 @@ -111,7 +110,7 @@ def __iter__(self): This method will return a generator that generates the clusters one by one.""" - clusters = [[] for _ in xrange(self._len)] + clusters = [[] for _ in range(self._len)] for idx, cluster in enumerate(self._membership): clusters[cluster].append(idx) return iter(clusters) @@ -171,7 +170,7 @@ def sizes(self, *args): return counts - def size_histogram(self, bin_width = 1): + def size_histogram(self, bin_width=1): """Returns the histogram of cluster sizes. @param bin_width: the bin width of the histogram @@ -190,19 +189,24 @@ def summary(self, verbosity=0, width=None): @return: the summary of the clustering as a string. """ out = StringIO() - print >>out, "Clustering with %d elements and %d clusters" % \ - (len(self._membership), len(self)) + print( + "Clustering with %d elements and %d clusters" + % ( + len(self._membership), + len(self), + ), + file=out, + ) if verbosity < 1: return out.getvalue().strip() ndigits = len(str(len(self))) - wrapper = _get_wrapper_for_width(width, - subsequent_indent = " " * (ndigits+3)) + wrapper = _get_wrapper_for_width(width, subsequent_indent=" " * (ndigits + 3)) for idx, cluster in enumerate(self._formatted_cluster_iterator()): wrapper.initial_indent = "[%*d] " % (ndigits, idx) - print >>out, "\n".join(wrapper.wrap(cluster)) + print("\n".join(wrapper.wrap(cluster)), file=out) return out.getvalue().strip() @@ -231,8 +235,14 @@ class VertexClustering(Clustering): # Allow None to be passed to __plot__ as the "palette" keyword argument _default_palette = None - def __init__(self, graph, membership = None, modularity = None, \ - params = None, modularity_params = None): + def __init__( + self, + graph, + membership=None, + modularity=None, + params=None, + modularity_params=None, + ): """Creates a clustering object for a given graph. @param graph: the graph that will be associated to the clustering @@ -248,7 +258,7 @@ def __init__(self, graph, membership = None, modularity = None, \ containing a C{weight} key with the appropriate value here. """ if membership is None: - Clustering.__init__(self, [0]*graph.vcount(), params) + Clustering.__init__(self, [0] * graph.vcount(), params) else: if len(membership) != graph.vcount(): raise ValueError("membership list has invalid length") @@ -355,8 +365,9 @@ def crossing(self): """Returns a boolean vector where element M{i} is C{True} iff edge M{i} lies between clusters, C{False} otherwise.""" membership = self.membership - return [membership[v1] != membership[v2] \ - for v1, v2 in self.graph.get_edgelist()] + return [ + membership[v1] != membership[v2] for v1, v2 in self.graph.get_edgelist() + ] @property def modularity(self): @@ -364,6 +375,7 @@ def modularity(self): if self._modularity_dirty: return self._recalculate_modularity_safe() return self._modularity + q = modularity @property @@ -381,8 +393,9 @@ def recalculate_modularity(self): @return: the new modularity score """ - self._modularity = self._graph.modularity(self._membership, - **self._modularity_params) + self._modularity = self._graph.modularity( + self._membership, **self._modularity_params + ) self._modularity_dirty = False return self._modularity @@ -410,7 +423,6 @@ def subgraph(self, idx): """ return self._graph.subgraph(self[idx]) - def subgraphs(self): """Gets all the subgraphs belonging to each of the clusters. @@ -420,7 +432,6 @@ def subgraphs(self): """ return [self._graph.subgraph(cl) for cl in self] - def giant(self): """Returns the largest cluster of the clustered graph. @@ -491,8 +502,9 @@ def __plot__(self, context, bbox, palette, *args, **kwds): if "edge_color" not in kwds and "color" not in self.graph.edge_attributes(): # Set up a default edge coloring based on internal vs external edges colors = ["grey20", "grey80"] - kwds["edge_color"] = [colors[is_crossing] - for is_crossing in self.crossing()] + kwds["edge_color"] = [ + colors[is_crossing] for is_crossing in self.crossing() + ] if palette is None: palette = ClusterColoringPalette(len(self)) @@ -502,7 +514,8 @@ def __plot__(self, context, bbox, palette, *args, **kwds): kwds["mark_groups"] = self else: kwds["mark_groups"] = _handle_mark_groups_arg_for_clustering( - kwds["mark_groups"], self) + kwds["mark_groups"], self + ) if "vertex_color" not in kwds: kwds["vertex_color"] = self.membership @@ -523,6 +536,7 @@ def _formatted_cluster_iterator(self): ############################################################################### + class Dendrogram(object): """The hierarchical clustering (dendrogram) of some dataset. @@ -563,7 +577,7 @@ def __init__(self, merges): self._merges = [tuple(pair) for pair in merges] self._nmerges = len(self._merges) if self._nmerges: - self._nitems = max(self._merges[-1])-self._nmerges+2 + self._nitems = max(self._merges[-1]) - self._nmerges + 2 else: self._nitems = 0 self._names = None @@ -577,7 +591,7 @@ def _convert_matrix_to_tuple_repr(merges, n=None): @return: the tuple representation of the clustering """ if n is None: - n = len(merges)+1 + n = len(merges) + 1 tuple_repr = range(n) idxs = range(n) for rowidx, row in enumerate(merges): @@ -587,8 +601,10 @@ def _convert_matrix_to_tuple_repr(merges, n=None): tuple_repr[idxi] = (tuple_repr[idxi], tuple_repr[idxj]) tuple_repr[idxj] = None except IndexError: - raise ValueError("malformed matrix, subgroup referenced "+ - "before being created in step %d" % rowidx) + raise ValueError( + "malformed matrix, subgroup referenced " + + "before being created in step %d" % rowidx + ) idxs.append(j) return [x for x in tuple_repr if x is not None] @@ -602,7 +618,7 @@ def _traverse_inorder(self): result = [] seen_nodes = set() - for node_index in reversed(xrange(self._nitems+self._nmerges)): + for node_index in reversed(range(self._nitems + self._nmerges)): if node_index in seen_nodes: continue @@ -617,7 +633,7 @@ def _traverse_inorder(self): else: # 'last' is a merge node, so let us proceed with the entry # where this merge node was created - stack.extend(self._merges[last-self._nitems]) + stack.extend(self._merges[last - self._nitems]) return result @@ -645,7 +661,7 @@ def format(self, format="newick"): else: nodes = list(self._names) if len(nodes) < n: - nodes.extend("" for _ in xrange(n - len(nodes))) + nodes.extend("" for _ in range(n - len(nodes))) for k, (i, j) in enumerate(self._merges, self._nitems): nodes[k] = "(%s,%s)%s" % (nodes[i], nodes[j], nodes[k]) nodes[i] = nodes[j] = None @@ -668,13 +684,15 @@ def summary(self, verbosity=0, max_leaf_count=40): @return: the summary of the dendrogram as a string. """ out = StringIO() - print >>out, "Dendrogram, %d elements, %d merges" % \ - (self._nitems, self._nmerges) + print("Dendrogram, %d elements, %d merges" % ( + self._nitems, + self._nmerges, + ), file=out) if self._nitems == 0 or verbosity < 1 or self._nitems > max_leaf_count: return out.getvalue().strip() - print >>out + print('', file=out) positions = [None] * self._nitems inorder = self._traverse_inorder() @@ -684,12 +702,12 @@ def summary(self, verbosity=0, max_leaf_count=40): for idx, element in enumerate(inorder): positions[element] = nextp inorder[idx] = str(element) - nextp += max(distance, len(inorder[idx])+1) + nextp += max(distance, len(inorder[idx]) + 1) - width = max(positions)+1 + width = max(positions) + 1 # Print the nodes on the lowest level - print >>out, (" " * (distance-1)).join(inorder) + print((" " * (distance - 1)).join(inorder), file=out) midx = 0 max_community_idx = self._nitems while midx < self._nmerges: @@ -698,8 +716,8 @@ def summary(self, verbosity=0, max_leaf_count=40): if position >= 0: char_array[position] = "|" char_str = "".join(char_array) - for _ in xrange(level_distance-1): - print >>out, char_str # Print the lines + for _ in range(level_distance - 1): + print(char_str, file=out) # Print the lines cidx_incr = 0 while midx < self._nmerges: @@ -713,16 +731,16 @@ def summary(self, verbosity=0, max_leaf_count=40): if pos1 > pos2: pos1, pos2 = pos2, pos1 - positions.append((pos1+pos2) // 2) + positions.append((pos1 + pos2) // 2) dashes = "-" * (pos2 - pos1 - 1) - char_array[pos1:(pos2+1)] = "`%s'" % dashes + char_array[pos1 : (pos2 + 1)] = "`%s'" % dashes cidx_incr += 1 max_community_idx += cidx_incr - print >>out, "".join(char_array) + print("".join(char_array), file=out) return out.getvalue().strip() @@ -732,7 +750,9 @@ def _item_box_size(self, context, horiz, idx): if self._names is None or self._names[idx] is None: x_bearing, _, _, height, x_advance, _ = context.text_extents("") else: - x_bearing, _, _, height, x_advance, _ = context.text_extents(str(self._names[idx])) + x_bearing, _, _, height, x_advance, _ = context.text_extents( + str(self._names[idx]) + ) if horiz: return x_advance - x_bearing, height @@ -753,12 +773,12 @@ def _plot_item(self, context, horiz, idx, x, y): height = self._item_box_size(context, True, idx)[1] if horiz: - context.move_to(x, y+height) + context.move_to(x, y + height) context.show_text(str(self._names[idx])) else: context.save() context.translate(x, y) - context.rotate(-pi/2.) + context.rotate(-pi / 2.0) context.move_to(0, height) context.show_text(str(self._names[idx])) context.restore() @@ -783,10 +803,11 @@ def __plot__(self, context, bbox, palette, *args, **kwds): from igraph.layout import Layout if self._names is None: - self._names = [str(x) for x in xrange(self._nitems)] + self._names = [str(x) for x in range(self._nitems)] - orientation = str_to_orientation(kwds.get("orientation", "lr"), - reversed_vertical=True) + orientation = str_to_orientation( + kwds.get("orientation", "lr"), reversed_vertical=True + ) horiz = orientation in ("lr", "rl") # Get the font height @@ -794,14 +815,15 @@ def __plot__(self, context, bbox, palette, *args, **kwds): # Calculate space needed for individual items at the # bottom of the dendrogram - item_boxes = [self._item_box_size(context, horiz, idx) \ - for idx in xrange(self._nitems)] + item_boxes = [ + self._item_box_size(context, horiz, idx) for idx in range(self._nitems) + ] # Small correction for cases when the right edge of the labels is # aligned with the tips of the dendrogram branches ygap = 2 if orientation == "bt" else 0 xgap = 2 if orientation == "lr" else 0 - item_boxes = [(x+xgap, y+ygap) for x, y in item_boxes] + item_boxes = [(x + xgap, y + ygap) for x, y in item_boxes] # Calculate coordinates layout = Layout([(0, 0)] * self._nitems, dim=2) @@ -814,7 +836,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): for id1, id2 in self._merges: y += 1 - layout.append(((layout[id1][0]+layout[id2][0])/2., y)) + layout.append(((layout[id1][0] + layout[id2][0]) / 2.0, y)) # Mirror or rotate the layout if necessary if orientation == "bt": @@ -827,7 +849,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): for id1, id2 in self._merges: x += 1 - layout.append((x, (layout[id1][1]+layout[id2][1])/2.)) + layout.append((x, (layout[id1][1] + layout[id2][1]) / 2.0)) # Mirror or rotate the layout if necessary if orientation == "rl": @@ -852,29 +874,31 @@ def __plot__(self, context, bbox, palette, *args, **kwds): delta_y = maxh if horiz: - delta_y += font_height / 2. + delta_y += font_height / 2.0 else: - delta_x += font_height / 2. - layout.fit_into((delta_x, delta_y, width - delta_x, height - delta_y), - keep_aspect_ratio=False) + delta_x += font_height / 2.0 + layout.fit_into( + (delta_x, delta_y, width - delta_x, height - delta_y), + keep_aspect_ratio=False, + ) context.save() context.translate(bbox.left, bbox.top) - context.set_source_rgb(0., 0., 0.) + context.set_source_rgb(0.0, 0.0, 0.0) context.set_line_width(1) # Draw items if horiz: sgn = 0 if orientation == "rl" else -1 - for idx in xrange(self._nitems): + for idx in range(self._nitems): x = layout[idx][0] + sgn * item_boxes[idx][0] - y = layout[idx][1] - item_boxes[idx][1]/2. + y = layout[idx][1] - item_boxes[idx][1] / 2.0 self._plot_item(context, horiz, idx, x, y) else: sgn = 1 if orientation == "bt" else 0 - for idx in xrange(self._nitems): - x = layout[idx][0] - item_boxes[idx][0]/2. + for idx in range(self._nitems): + x = layout[idx][0] - item_boxes[idx][0] / 2.0 y = layout[idx][1] + sgn * item_boxes[idx][1] self._plot_item(context, horiz, idx, x, y) @@ -926,15 +950,16 @@ def names(self, items): n = self._nitems + self._nmerges self._names = items[:n] if len(self._names) < n: - self._names.extend("" for _ in xrange(n-len(self._names))) + self._names.extend("" for _ in range(n - len(self._names))) class VertexDendrogram(Dendrogram): """The dendrogram resulting from the hierarchical clustering of the vertex set of a graph.""" - def __init__(self, graph, merges, optimal_count = None, params = None, - modularity_params = None): + def __init__( + self, graph, merges, optimal_count=None, params=None, modularity_params=None + ): """Creates a dendrogram object for a given graph. @param graph: the graph that will be associated to the clustering @@ -974,11 +999,11 @@ def as_clustering(self, n=None): n = self.optimal_count num_elts = self._graph.vcount() idgen = UniqueIdGenerator() - membership = community_to_membership(self._merges, num_elts, \ - num_elts - n) + membership = community_to_membership(self._merges, num_elts, num_elts - n) membership = [idgen[m] for m in membership] - return VertexClustering(self._graph, membership, - modularity_params=self._modularity_params) + return VertexClustering( + self._graph, membership, modularity_params=self._modularity_params + ) @property def optimal_count(self): @@ -994,11 +1019,11 @@ def optimal_count(self): n = self._graph.vcount() max_q, optimal_count = 0, 1 - for step in xrange(min(n-1, len(self._merges))): + for step in range(min(n - 1, len(self._merges))): membs = community_to_membership(self._merges, n, step) q = self._graph.modularity(membs, **self._modularity_params) if q > max_q: - optimal_count = n-step + optimal_count = n - step max_q = q self._optimal_count = optimal_count return optimal_count @@ -1020,16 +1045,19 @@ class VisualVertexBuilder(AttributeCollectorBase): builder = VisualVertexBuilder(self._graph.vs, kwds) self._names = [vertex.label for vertex in builder] - self._names = [name if name is not None else str(idx) - for idx, name in enumerate(self._names)] - result = Dendrogram.__plot__(self, context, bbox, palette, \ - *args, **kwds) + self._names = [ + name if name is not None else str(idx) + for idx, name in enumerate(self._names) + ] + result = Dendrogram.__plot__(self, context, bbox, palette, *args, **kwds) del self._names return result + ############################################################################### + class Cover(object): """Class representing a cover of an arbitrary ordered set. @@ -1107,7 +1135,7 @@ def __init__(self, clusters, n=0): self._clusters = [list(cluster) for cluster in clusters] try: - self._n = max(max(cluster)+1 for cluster in self._clusters if cluster) + self._n = max(max(cluster) + 1 for cluster in self._clusters if cluster) except ValueError: self._n = 0 self._n = max(n, self._n) @@ -1136,7 +1164,7 @@ def membership(self): length I{n}, where element I{i} contains the cluster indices of the I{i}th item. """ - result = [[] for _ in xrange(self._n)] + result = [[] for _ in range(self._n)] for idx, cluster in enumerate(self): for item in cluster: result[item].append(idx) @@ -1164,7 +1192,7 @@ def sizes(self, *args): return [len(self._clusters[idx]) for idx in args] return [len(cluster) for cluster in self] - def size_histogram(self, bin_width = 1): + def size_histogram(self, bin_width=1): """Returns the histogram of cluster sizes. @param bin_width: the bin width of the histogram @@ -1183,18 +1211,17 @@ def summary(self, verbosity=0, width=None): @return: the summary of the cover as a string. """ out = StringIO() - print >>out, "Cover with %d clusters" % len(self) + print("Cover with %d clusters" % len(self), file=out) if verbosity < 1: return out.getvalue().strip() ndigits = len(str(len(self))) - wrapper = _get_wrapper_for_width(width, - subsequent_indent = " " * (ndigits+3)) + wrapper = _get_wrapper_for_width(width, subsequent_indent=" " * (ndigits + 3)) for idx, cluster in enumerate(self._formatted_cluster_iterator()): wrapper.initial_indent = "[%*d] " % (ndigits, idx) - print >>out, "\n".join(wrapper.wrap(cluster)) + print("\n".join(wrapper.wrap(cluster)), file=out) return out.getvalue().strip() @@ -1219,7 +1246,7 @@ class VertexCover(Cover): @undocumented: _formatted_cluster_iterator """ - def __init__(self, graph, clusters = None): + def __init__(self, graph, clusters=None): """Creates a cover object for a given graph. @param graph: the graph that will be associated to the cover @@ -1229,10 +1256,12 @@ def __init__(self, graph, clusters = None): if clusters is None: clusters = [range(graph.vcount())] - Cover.__init__(self, clusters, n = graph.vcount()) + Cover.__init__(self, clusters, n=graph.vcount()) if self._n > graph.vcount(): - raise ValueError("cluster list contains vertex ID larger than the " - "number of vertices in the graph") + raise ValueError( + "cluster list contains vertex ID larger than the " + "number of vertices in the graph" + ) self._graph = graph @@ -1240,8 +1269,10 @@ def crossing(self): """Returns a boolean vector where element M{i} is C{True} iff edge M{i} lies between clusters, C{False} otherwise.""" membership = [frozenset(cluster) for cluster in self.membership] - return [membership[v1].isdisjoint(membership[v2]) \ - for v1, v2 in self.graph.get_edgelist()] + return [ + membership[v1].isdisjoint(membership[v2]) + for v1, v2 in self.graph.get_edgelist() + ] @property def graph(self): @@ -1318,8 +1349,9 @@ def __plot__(self, context, bbox, palette, *args, **kwds): if "edge_color" not in kwds and "color" not in self.graph.edge_attributes(): # Set up a default edge coloring based on internal vs external edges colors = ["grey20", "grey80"] - kwds["edge_color"] = [colors[is_crossing] - for is_crossing in self.crossing()] + kwds["edge_color"] = [ + colors[is_crossing] for is_crossing in self.crossing() + ] if "palette" in kwds: palette = kwds["palette"] @@ -1331,7 +1363,8 @@ def __plot__(self, context, bbox, palette, *args, **kwds): kwds["mark_groups"] = self else: kwds["mark_groups"] = _handle_mark_groups_arg_for_clustering( - kwds["mark_groups"], self) + kwds["mark_groups"], self + ) return self._graph.__plot__(context, bbox, palette, *args, **kwds) @@ -1358,7 +1391,7 @@ class CohesiveBlocks(VertexCover): block structures easier. """ - def __init__(self, graph, blocks = None, cohesion = None, parent = None): + def __init__(self, graph, blocks=None, cohesion=None, parent=None): """Constructs a new cohesive block structure for the given graph. If any of I{blocks}, I{cohesion} or I{parent} is C{None}, all the @@ -1406,15 +1439,17 @@ def hierarchy(self): In other words, the edges point downwards. """ from igraph import Graph - edges = [pair for pair in izip(self._parent, xrange(len(self))) - if pair[0] is not None] + + edges = [ + pair for pair in zip(self._parent, range(len(self))) if pair[0] is not None + ] return Graph(edges, directed=True) def max_cohesion(self, idx): """Finds the maximum cohesion score among all the groups that contain the given vertex.""" result = 0 - for cohesion, cluster in izip(self._cohesion, self._clusters): + for cohesion, cluster in zip(self._cohesion, self._clusters): if idx in cluster: result = max(result, cohesion) return result @@ -1423,7 +1458,7 @@ def max_cohesions(self): """For each vertex in the graph, returns the maximum cohesion score among all the groups that contain the vertex.""" result = [0] * self._graph.vcount() - for cohesion, cluster in izip(self._cohesion, self._clusters): + for cohesion, cluster in zip(self._cohesion, self._clusters): for idx in cluster: result[idx] = max(result[idx], cohesion) return result @@ -1458,8 +1493,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): prepare_groups = True if prepare_groups: - colors = [pair for pair in enumerate(self.cohesions()) - if pair[1] > 1] + colors = [pair for pair in enumerate(self.cohesions()) if pair[1] > 1] kwds["mark_groups"] = colors if "vertex_color" not in kwds: @@ -1467,6 +1501,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): return VertexCover.__plot__(self, context, bbox, palette, *args, **kwds) + def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): """Handles the mark_groups=... keyword argument in plotting methods of clusterings. @@ -1496,8 +1531,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): if isinstance(first, (int, long)): # Yes. Seems like we have a list of cluster indices. # Assign color indices automatically. - group_iter = ((group, color) - for color, group in enumerate(mark_groups)) + group_iter = ((group, color) for color, group in enumerate(mark_groups)) else: # No. Seems like we have good ol' group-color pairs. group_iter = mark_groups @@ -1517,8 +1551,10 @@ def cluster_index_resolver(): return cluster_index_resolver() + ############################################################## + def _prepare_community_comparison(comm1, comm2, remove_none=False): """Auxiliary method that takes two community structures either as membership lists or instances of L{Clustering}, and returns a @@ -1536,6 +1572,7 @@ def _prepare_community_comparison(comm1, comm2, remove_none=False): C{None} values are filtered away and only the remaining lists are compared. """ + def _ensure_list(obj): if isinstance(obj, Clustering): return obj.membership @@ -1546,8 +1583,9 @@ def _ensure_list(obj): raise ValueError("the two membership vectors must be equal in length") if remove_none and (None in vec1 or None in vec2): - idxs_to_remove = [i for i in xrange(len(vec1)) \ - if vec1[i] is None or vec2[i] is None] + idxs_to_remove = [ + i for i in range(len(vec1)) if vec1[i] is None or vec2[i] is None + ] idxs_to_remove.reverse() n = len(vec1) for i in idxs_to_remove: @@ -1601,6 +1639,7 @@ def compare_communities(comm1, comm2, method="vi", remove_none=False): Classification 2:193-218, 1985. """ import igraph._igraph + vec1, vec2 = _prepare_community_comparison(comm1, comm2, remove_none) return igraph._igraph._compare_communities(vec1, vec2, method) @@ -1656,5 +1695,6 @@ def split_join_distance(comm1, comm2, remove_none=False): sum of them. """ import igraph._igraph + vec1, vec2 = _prepare_community_comparison(comm1, comm2, remove_none) return igraph._igraph._split_join_distance(vec1, vec2) diff --git a/src/igraph/compat.py b/src/igraph/compat.py deleted file mode 100644 index 5c9bbc71a..000000000 --- a/src/igraph/compat.py +++ /dev/null @@ -1,75 +0,0 @@ -# vim:ts=4:sw=4:sts=4:et -# -*- coding: utf-8 -*- -""" -Compatibility methods and backported versions of newer Python features -to enable igraph to run on Python 2.5. -""" - -import sys - -__license__ = u"""\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - -############################################################################# -# Simulating math.isnan - -try: - from math import isnan -except ImportError: - def isnan(num): - return num != num - -############################################################################# -# Providing @property.setter syntax for Python 2.5 - -if sys.version_info < (2, 6): - _property = property - - class property(property): - def __init__(self, fget, *args, **kwds): - self.__doc__ = fget.__doc__ - super(property, self).__init__(fget, *args, **kwds) - - def setter(self, fset): - cls_ns = sys._getframe(1).f_locals - for k, v in cls_ns.iteritems(): - if v == self: - propname = k - break - cls_ns[propname] = property(self.fget, fset, self.fdel, self.__doc__) - return cls_ns[propname] -else: - if isinstance(__builtins__, dict): - # This branch is for CPython - property = __builtins__["property"] - else: - # This branch is for PyPy - property = __builtins__.property - -############################################################################# -# Providing BytesIO for Python 2.5 - -try: - from io import BytesIO -except ImportError: - # We are on Python 2.5 or earlier because Python 2.6 has a BytesIO - # class already - from cStringIO import StringIO - BytesIO = StringIO diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 4a50ce4ea..2a6921f21 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -29,8 +29,9 @@ """ import sys + if sys.version_info < (3, 2): - from ConfigParser import SafeConfigParser as ConfigParser + from configparser import SafeConfigParser as ConfigParser else: from configparser import ConfigParser @@ -46,9 +47,21 @@ def get_platform_image_viewer(): return "open" elif plat == "Linux": # Linux has a whole lot of choices, try to find one - choices = ["eog", "gthumb", "gqview", "kuickshow", "xnview", "display", - "gpicview", "gwenview", "qiv", "gimv", "ristretto", - "geeqie", "eom"] + choices = [ + "eog", + "gthumb", + "gqview", + "kuickshow", + "xnview", + "display", + "gpicview", + "gwenview", + "qiv", + "gimv", + "ristretto", + "geeqie", + "eom", + ] paths = ["/usr/bin", "/bin"] for path in paths: for choice in choices: @@ -58,9 +71,19 @@ def get_platform_image_viewer(): return "" elif plat == "FreeBSD": # FreeBSD also has a whole lot of choices, try to find one - choices = ["eog", "gthumb", "geeqie", "display", - "gpicview", "gwenview", "qiv", "gimv", "ristretto", - "geeqie", "eom"] + choices = [ + "eog", + "gthumb", + "geeqie", + "display", + "gpicview", + "gwenview", + "qiv", + "gimv", + "ristretto", + "geeqie", + "eom", + ] paths = ["%%LOCALBASE%%/bin"] for path in paths: for choice in choices: @@ -68,7 +91,7 @@ def get_platform_image_viewer(): if os.path.isfile(full_path): return full_path return "" - elif plat == "Windows" or plat == "Microsoft": # Thanks to Dale Hunscher + elif plat == "Windows" or plat == "Microsoft": # Thanks to Dale Hunscher # Use the built-in Windows image viewer, if available return "start" else: @@ -232,53 +255,21 @@ def setfloat(obj, section, key, value): obj.set(section, key, str(float(value))) _types = { - "boolean": { - "getter": ConfigParser.getboolean, - "setter": Types.setboolean - }, - "int": { - "getter": ConfigParser.getint, - "setter": Types.setint - }, - "float": { - "getter": ConfigParser.getfloat, - "setter": Types.setfloat - } + "boolean": {"getter": ConfigParser.getboolean, "setter": Types.setboolean}, + "int": {"getter": ConfigParser.getint, "setter": Types.setint}, + "float": {"getter": ConfigParser.getfloat, "setter": Types.setfloat}, } _sections = ("general", "apps", "plotting", "remote", "shell") _definitions = { - "general.shells": { - "default": "IPythonShell,ClassicPythonShell" - }, - "general.verbose": { - "default": True, - "type": "boolean" - }, - - "apps.image_viewer": { - "default": get_platform_image_viewer() - }, - - "plotting.layout": { - "default": "auto" - }, - "plotting.mark_groups": { - "default": False, - "type": "boolean" - }, - "plotting.palette": { - "default": "gray" - }, - "plotting.wrap_labels": { - "default": False, - "type": "boolean" - }, - - "shell.ipython.inlining.Plot": { - "default": True, - "type": "boolean" - } + "general.shells": {"default": "IPythonShell,ClassicPythonShell"}, + "general.verbose": {"default": True, "type": "boolean"}, + "apps.image_viewer": {"default": get_platform_image_viewer()}, + "plotting.layout": {"default": "auto"}, + "plotting.mark_groups": {"default": False, "type": "boolean"}, + "plotting.palette": {"default": "gray"}, + "plotting.wrap_labels": {"default": False, "type": "boolean"}, + "shell.ipython.inlining.Plot": {"default": True, "type": "boolean"}, } # The singleton instance we are using throughout other modules @@ -296,7 +287,7 @@ def __init__(self, filename=None): for sec in self._sections: self._config.add_section(sec) # Create default values - for name, definition in self._definitions.iteritems(): + for name, definition in self._definitions.items(): if "default" in definition: self[name] = definition["default"] @@ -410,7 +401,7 @@ def load(self, stream=None): loaded. """ stream = stream or get_user_config_file() - if isinstance(stream, basestring): + if isinstance(stream, str): stream = open(stream, "r") file_was_open = True self._config.readfp(stream) @@ -464,4 +455,3 @@ def init(): @return: the L{Configuration} object loaded or created.""" return Configuration.instance() - diff --git a/src/igraph/cut.py b/src/igraph/cut.py index 6d0100ab8..0acbaf500 100644 --- a/src/igraph/cut.py +++ b/src/igraph/cut.py @@ -24,13 +24,14 @@ 02110-1301 USA """ + class Cut(VertexClustering): """A cut of a given graph. This is a simple class used to represent cuts returned by L{Graph.mincut()}, L{Graph.all_st_cuts()} and other functions that calculate cuts. - + A cut is a special vertex clustering with only two clusters. Besides the usual L{VertexClustering} methods, it also has the following attributes: @@ -66,8 +67,7 @@ class Cut(VertexClustering): """ # pylint: disable-msg=R0913 - def __init__(self, graph, value=None, cut=None, partition=None, - partition2=None): + def __init__(self, graph, value=None, cut=None, partition=None, partition2=None): """Initializes the cut. This should not be called directly, everything is taken care of by @@ -92,14 +92,21 @@ def __init__(self, graph, value=None, cut=None, partition=None, self._cut = cut def __repr__(self): - return "%s(%r, %r, %r, %r)" % \ - (self.__class__.__name__, self._graph, \ - self._value, self._cut, self._partition) + return "%s(%r, %r, %r, %r)" % ( + self.__class__.__name__, + self._graph, + self._value, + self._cut, + self._partition, + ) def __str__(self): - return "Graph cut (%d edges, %d vs %d vertices, value=%.4f)" % \ - (len(self._cut), len(self._partition), - self._graph.vcount() - len(self._partition), self._value) + return "Graph cut (%d edges, %d vs %d vertices, value=%.4f)" % ( + len(self._cut), + len(self._partition), + self._graph.vcount() - len(self._partition), + self._value, + ) # pylint: disable-msg=C0103 @property @@ -131,7 +138,7 @@ class Flow(Cut): - C{graph} - the graph on which this flow is defined - - C{value} - the value (capacity) of the flow + - C{value} - the value (capacity) of the flow - C{flow} - the flow values on each edge. For directed graphs, this is simply a list where element M{i} corresponds to the @@ -174,19 +181,27 @@ def __init__(self, graph, value, flow, cut, partition): self._flow = flow def __repr__(self): - return "%s(%r, %r, %r, %r, %r)" % \ - (self.__class__.__name__, self._graph, \ - self._value, self._flow, self._cut, self._partition) + return "%s(%r, %r, %r, %r, %r)" % ( + self.__class__.__name__, + self._graph, + self._value, + self._flow, + self._cut, + self._partition, + ) def __str__(self): - return "Graph flow (%d edges, %d vs %d vertices, value=%.4f)" % \ - (len(self._cut), len(self._partition), - self._graph.vcount() - len(self._partition), self._value) + return "Graph flow (%d edges, %d vs %d vertices, value=%.4f)" % ( + len(self._cut), + len(self._partition), + self._graph.vcount() - len(self._partition), + self._value, + ) @property def flow(self): """Returns the flow values for each edge. - + For directed graphs, this is simply a list where element M{i} corresponds to the flow on edge M{i}. For undirected graphs, the direction of the flow is not constrained (since the edges are @@ -195,6 +210,3 @@ def flow(self): larger vertex ID to the smaller. """ return self._flow - - - diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index b7f3e1387..b2bf6a17d 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -24,6 +24,7 @@ 02110-1301 USA """ + class Matrix(object): """Simple matrix data type. @@ -63,7 +64,7 @@ def Fill(cls, value, *args): height, width = int(args[0]), int(args[0]) else: height, width = int(args[0]), int(args[1]) - mtrx = [[value]*width for _ in xrange(height)] + mtrx = [[value] * width for _ in range(height)] return cls(mtrx) # pylint: disable-msg=C0103 @@ -89,7 +90,7 @@ def Identity(cls, *args): """ # pylint: disable-msg=W0212 result = cls.Fill(0, *args) - for i in xrange(min(result.shape)): + for i in range(min(result.shape)): result._data[i][i] = 1 return result @@ -104,11 +105,12 @@ def _set_data(self, data=None): self._ncol = 0 for row in self._data: if len(row) < self._ncol: - row.extend([0]*(self._ncol-len(row))) + row.extend([0] * (self._ncol - len(row))) def _get_data(self): """Returns the data stored in the matrix as a list of lists""" return [list(row) for row in self._data] + data = property(_get_data, _set_data) @property @@ -127,20 +129,23 @@ def __add__(self, other): if isinstance(other, Matrix): if self.shape != other.shape: raise ValueError("matrix shapes do not match") - return self.__class__([ - [a+b for a, b in izip(row_a, row_b)] - for row_a, row_b in izip(self, other) - ]) + return self.__class__( + [ + [a + b for a, b in zip(row_a, row_b)] + for row_a, row_b in zip(self, other) + ] + ) else: - return self.__class__([ - [item+other for item in row] for row in self]) + return self.__class__([[item + other for item in row] for row in self]) def __eq__(self, other): """Checks whether a given matrix is equal to another one""" - return isinstance(other, Matrix) and \ - self._nrow == other._nrow and \ - self._ncol == other._ncol and \ - self._data == other._data + return ( + isinstance(other, Matrix) + and self._nrow == other._nrow + and self._ncol == other._ncol + and self._data == other._data + ) def __getitem__(self, i): """Returns a single item, a row or a column of the matrix @@ -181,12 +186,12 @@ def __iadd__(self, other): if isinstance(other, Matrix): if self.shape != other.shape: raise ValueError("matrix shapes do not match") - for row_a, row_b in izip(self._data, other): - for i in xrange(len(row_a)): + for row_a, row_b in zip(self._data, other): + for i in range(len(row_a)): row_a[i] += row_b[i] else: for row in self._data: - for i in xrange(len(row)): + for i in range(len(row)): row[i] += other return self @@ -195,12 +200,12 @@ def __isub__(self, other): if isinstance(other, Matrix): if self.shape != other.shape: raise ValueError("matrix shapes do not match") - for row_a, row_b in izip(self._data, other): - for i in xrange(len(row_a)): + for row_a, row_b in zip(self._data, other): + for i in range(len(row_a)): row_a[i] -= row_b[i] else: for row in self._data: - for i in xrange(len(row)): + for i in range(len(row)): row[i] -= other return self @@ -227,8 +232,7 @@ def __setitem__(self, i, value): if len(value) != len(self._data[i]): raise ValueError("new value must have %d items" % self._ncol) if any(len(row) != self._ncol for row in value): - raise ValueError("rows of new value must have %d items" % \ - self._ncol) + raise ValueError("rows of new value must have %d items" % self._ncol) self._data[i] = [list(row) for row in value] elif isinstance(i, tuple): try: @@ -263,13 +267,14 @@ def __sub__(self, other): if isinstance(other, Matrix): if self.shape != other.shape: raise ValueError("matrix shapes do not match") - return self.__class__([ - [a-b for a, b in izip(row_a, row_b)] - for row_a, row_b in izip(self, other) - ]) + return self.__class__( + [ + [a - b for a, b in zip(row_a, row_b)] + for row_a, row_b in zip(self, other) + ] + ) else: - return self.__class__([ - [item-other for item in row] for row in self]) + return self.__class__([[item - other for item in row] for row in self]) def __repr__(self): class_name = self.__class__.__name__ @@ -343,8 +348,8 @@ def __plot__(self, context, bbox, palette, **kwds): """ # pylint: disable-msg=W0142 # pylint: disable-msg=C0103 - grid_width = float(kwds.get("grid_width", 1.)) - border_width = float(kwds.get("border_width", 1.)) + grid_width = float(kwds.get("grid_width", 1.0)) + border_width = float(kwds.get("border_width", 1.0)) style = kwds.get("style", "boolean") row_names = kwds.get("row_names") col_names = kwds.get("col_names", row_names) @@ -361,11 +366,11 @@ def __plot__(self, context, bbox, palette, **kwds): if row_names is not None: row_names = [str(name) for name in islice(row_names, self._nrow)] if len(row_names) < self._nrow: - row_names.extend([""]*(self._nrow-len(row_names))) + row_names.extend([""] * (self._nrow - len(row_names))) if col_names is not None: col_names = [str(name) for name in islice(col_names, self._ncol)] if len(col_names) < self._ncol: - col_names.extend([""]*(self._ncol-len(col_names))) + col_names.extend([""] * (self._ncol - len(col_names))) if values == False: values = None if values == True: @@ -381,19 +386,19 @@ def __plot__(self, context, bbox, palette, **kwds): if row_names is not None or col_names is not None: te = context.text_extents space_width = te(" ")[4] - max_row_name_width = max([te(s)[4] for s in row_names])+space_width - max_col_name_width = max([te(s)[4] for s in col_names])+space_width + max_row_name_width = max([te(s)[4] for s in row_names]) + space_width + max_col_name_width = max([te(s)[4] for s in col_names]) + space_width else: max_row_name_width, max_col_name_width = 0, 0 # Calculate sizes - total_width = float(bbox.width)-max_row_name_width - total_height = float(bbox.height)-max_col_name_width + total_width = float(bbox.width) - max_row_name_width + total_height = float(bbox.height) - max_col_name_width dx = total_width / self.shape[1] dy = total_height / self.shape[0] if kwds.get("square", True): dx, dy = min(dx, dy), min(dx, dy) - total_width, total_height = dx*self.shape[1], dy*self.shape[0] + total_width, total_height = dx * self.shape[1], dy * self.shape[0] ox = bbox.left + (bbox.width - total_width - max_row_name_width) / 2.0 oy = bbox.top + (bbox.height - total_height - max_col_name_width) / 2.0 ox += max_row_name_width @@ -403,11 +408,11 @@ def __plot__(self, context, bbox, palette, **kwds): if style == "palette": mi, ma = self.min(), self.max() color_offset = mi - color_ratio = (len(palette)-1) / float(ma-mi) + color_ratio = (len(palette) - 1) / float(ma - mi) # Validate grid width - if dx < 3*grid_width or dy < 3*grid_width: - grid_width = 0. + if dx < 3 * grid_width or dy < 3 * grid_width: + grid_width = 0.0 if grid_width > 0: context.set_line_width(grid_width) else: @@ -418,12 +423,12 @@ def __plot__(self, context, bbox, palette, **kwds): context.set_line_width(1) # Draw row names (if any) - context.set_source_rgb(0., 0., 0.) + context.set_source_rgb(0.0, 0.0, 0.0) if row_names is not None: - x, y = ox, oy + x, y = ox, oy for heading in row_names: _, _, _, h, xa, _ = context.text_extents(heading) - context.move_to(x-xa-space_width, y + (dy+h)/2.) + context.move_to(x - xa - space_width, y + (dy + h) / 2.0) context.show_text(heading) y += dy @@ -431,11 +436,11 @@ def __plot__(self, context, bbox, palette, **kwds): if col_names is not None: context.save() context.translate(ox, oy) - context.rotate(-1.5707963285) # pi/2 - x, y = 0., 0. + context.rotate(-1.5707963285) # pi/2 + x, y = 0.0, 0.0 for heading in col_names: _, _, _, h, _, _ = context.text_extents(heading) - context.move_to(x+space_width, y + (dx+h)/2.) + context.move_to(x + space_width, y + (dx + h) / 2.0) context.show_text(heading) y += dx context.restore() @@ -453,11 +458,11 @@ def __plot__(self, context, bbox, palette, **kwds): continue if style == "boolean": if item: - context.set_source_rgb(0., 0., 0.) + context.set_source_rgb(0.0, 0.0, 0.0) else: - context.set_source_rgb(1., 1., 1.) + context.set_source_rgb(1.0, 1.0, 1.0) elif style == "palette": - cidx = int((item-color_offset)*color_ratio) + cidx = int((item - color_offset) * color_ratio) if cidx < 0: cidx = 0 context.set_source_rgba(*palette.get(cidx)) @@ -470,12 +475,12 @@ def __plot__(self, context, bbox, palette, **kwds): fill() context.stroke() x += dx - x, y = ox, y+dy + x, y = ox, y + dy # Draw cell values if values is not None: x, y = ox, oy - context.set_source_rgb(0., 0., 0.) + context.set_source_rgb(0.0, 0.0, 0.0) for row in values.data: if hasattr(value_format, "__call__"): values = [value_format(item) for item in row] @@ -483,19 +488,18 @@ def __plot__(self, context, bbox, palette, **kwds): values = [value_format % item for item in row] for item in values: th, tw = context.text_extents(item)[3:5] - context.move_to(x+(dx-tw)/2., y+(dy+th)/2.) + context.move_to(x + (dx - tw) / 2.0, y + (dy + th) / 2.0) context.show_text(item) x += dx - x, y = ox, y+dy + x, y = ox, y + dy # Draw borders if border_width > 0: context.set_line_width(border_width) - context.set_source_rgb(0., 0., 0.) - context.rectangle(ox, oy, dx*self.shape[1], dy*self.shape[0]) + context.set_source_rgb(0.0, 0.0, 0.0) + context.rectangle(ox, oy, dx * self.shape[1], dy * self.shape[0]) context.stroke() - def min(self, dim=None): """Returns the minimum of the matrix along the given dimension @@ -506,8 +510,7 @@ def min(self, dim=None): if dim == 1: return [min(row) for row in self._data] if dim == 0: - return [min(row[idx] for row in self._data) \ - for idx in xrange(self._ncol)] + return [min(row[idx] for row in self._data) for idx in range(self._ncol)] return min(min(row) for row in self._data) def max(self, dim=None): @@ -520,8 +523,7 @@ def max(self, dim=None): if dim == 1: return [max(row) for row in self._data] if dim == 0: - return [max(row[idx] for row in self._data) \ - for idx in xrange(self._ncol)] + return [max(row[idx] for row in self._data) for idx in range(self._ncol)] return max(max(row) for row in self._data) @@ -548,8 +550,18 @@ class DyadCensus(tuple): @undocumented: _remap """ - _remap = {"mutual": 0, "mut": 0, "sym": 0, "symm": 0, - "asy": 1, "asym": 1, "asymm": 1, "asymmetric": 1, "null": 2} + + _remap = { + "mutual": 0, + "mut": 0, + "sym": 0, + "symm": 0, + "asy": 1, + "asym": 1, + "asymm": 1, + "asymmetric": 1, + "null": 2, + } def __getitem__(self, idx): return tuple.__getitem__(self, self._remap.get(idx, idx)) @@ -608,18 +620,33 @@ class TriadCensus(tuple): >>> print tc["030C"] #doctest:+SKIP 1206 """ - _remap = {"003": 0, "012": 1, "102": 2, "021D": 3, "021U": 4, "021C": 5, \ - "111D": 6, "111U": 7, "030T": 8, "030C": 9, "201": 10, "120D": 11, \ - "120U": 12, "120C": 13, "210": 14, "300": 15} + + _remap = { + "003": 0, + "012": 1, + "102": 2, + "021D": 3, + "021U": 4, + "021C": 5, + "111D": 6, + "111U": 7, + "030T": 8, + "030C": 9, + "201": 10, + "120D": 11, + "120U": 12, + "120C": 13, + "210": 14, + "300": 15, + } def __getitem__(self, idx): - if isinstance(idx, basestring): + if isinstance(idx, str): idx = idx.upper() return tuple.__getitem__(self, self._remap.get(idx, idx)) def __getattr__(self, attr): - if isinstance(attr, basestring) and attr[0] == 't' \ - and attr[1:].upper() in self._remap: + if isinstance(attr, str) and attr[0] == "t" and attr[1:].upper() in self._remap: return tuple.__getitem__(self, self._remap[attr[1:].upper()]) raise AttributeError("no such attribute: %s" % attr) @@ -637,14 +664,16 @@ def __str__(self): if rowcount * colcount < maxidx: rowcount += 1 - invmap = dict((v, k) for k, v in self._remap.iteritems()) + invmap = dict((v, k) for k, v in self._remap.items()) result, row, idx = [], [], 0 - for _ in xrange(rowcount): - for _ in xrange(colcount): + for _ in range(rowcount): + for _ in range(colcount): if idx >= maxidx: - break - row.append("%-*s: %*d" % (captionwidth, invmap.get(idx, ""), - numwidth, self[idx])) + break + row.append( + "%-*s: %*d" + % (captionwidth, invmap.get(idx, ""), numwidth, self[idx]) + ) idx += 1 result.append(" | ".join(row)) row = [] @@ -657,7 +686,7 @@ class UniqueIdGenerator(object): names (say, vertex names). Usage: - + >>> gen = UniqueIdGenerator() >>> gen["A"] 0 @@ -688,6 +717,7 @@ def __init__(self, id_generator=None, initial=None): id_generator = 0 if isinstance(id_generator, int): import itertools + self._generator = itertools.count(id_generator) else: self._generator = id_generator @@ -706,7 +736,7 @@ def __getitem__(self, item): try: return self._ids[item] except KeyError: - self._ids[item] = self._generator.next() + self._ids[item] = next(self._generator) return self._ids[item] def __setitem__(self, item, value): @@ -720,15 +750,13 @@ def __len__(self): def reverse_dict(self): """Returns the reverse mapping, i.e., the one that maps from generated IDs to their corresponding objects""" - return dict((v, k) for k, v in self._ids.iteritems()) + return dict((v, k) for k, v in self._ids.items()) def values(self): """Returns the values stored so far. If the generator generates items according to the standard sorting order, the values returned will be exactly in the order they were added. This holds for integer IDs for instance (but for many other ID generators as well).""" - return sorted(self._ids.keys(), key = self._ids.__getitem__) + return sorted(list(self._ids.keys()), key=self._ids.__getitem__) add = __getitem__ - - diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 83b38c4e9..9e352fa31 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -15,20 +15,25 @@ replace it with C{cairocffi} as the two are API-compatible. """ -from __future__ import with_statement -from cStringIO import StringIO +from io import StringIO from warnings import warn import os import platform import time -from igraph.compat import property, BytesIO +from io import BytesIO from igraph.configuration import Configuration from igraph.drawing.colors import Palette, palettes from igraph.drawing.graph import DefaultGraphDrawer, MatplotlibGraphDrawer -from igraph.drawing.utils import BoundingBox, Point, Rectangle, find_cairo, find_matplotlib +from igraph.drawing.utils import ( + BoundingBox, + Point, + Rectangle, + find_cairo, + find_matplotlib, +) from igraph.utils import _is_running_in_ipython, named_temporary_file __all__ = ["BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot"] @@ -40,6 +45,7 @@ ##################################################################### + class Plot(object): """Class representing an arbitrary plot @@ -141,8 +147,9 @@ def __init__(self, target=None, bbox=None, palette=None, background=None): if target is None: self._need_tmpfile = True - self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \ - int(self.bbox.width), int(self.bbox.height)) + self._surface = cairo.ImageSurface( + cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) + ) elif isinstance(target, cairo.Surface): self._surface = target else: @@ -150,17 +157,21 @@ def __init__(self, target=None, bbox=None, palette=None, background=None): _, ext = os.path.splitext(target) ext = ext.lower() if ext == ".pdf": - self._surface = cairo.PDFSurface(target, self.bbox.width, \ - self.bbox.height) + self._surface = cairo.PDFSurface( + target, self.bbox.width, self.bbox.height + ) elif ext == ".ps" or ext == ".eps": - self._surface = cairo.PSSurface(target, self.bbox.width, \ - self.bbox.height) + self._surface = cairo.PSSurface( + target, self.bbox.width, self.bbox.height + ) elif ext == ".png": - self._surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, \ - int(self.bbox.width), int(self.bbox.height)) + self._surface = cairo.ImageSurface( + cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) + ) elif ext == ".svg": - self._surface = cairo.SVGSurface(target, self.bbox.width, \ - self.bbox.height) + self._surface = cairo.SVGSurface( + target, self.bbox.width, self.bbox.height + ) else: raise ValueError("image format not handled by Cairo: %s" % ext) @@ -235,12 +246,12 @@ def remove(self, obj, bbox=None, idx=1): C{False} if the object was not on the plot at all or M{idx} was larger than the count of occurrences """ - for i in xrange(len(self._objects)): + for i in range(len(self._objects)): current_obj, current_bbox = self._objects[i][0:2] if current_obj is obj and (bbox is None or current_bbox == bbox): idx -= 1 if idx == 0: - self._objects[i:(i+1)] = [] + self._objects[i : (i + 1)] = [] self.mark_dirty() return True return False @@ -264,7 +275,7 @@ def redraw(self, context=None): palette = getattr(obj, "_default_palette", self._palette) plotter = getattr(obj, "__plot__", None) if plotter is None: - warn("%s does not support plotting" % (obj, )) + warn("%s does not support plotting" % (obj,)) else: if opacity < 1.0: ctx.push_group() @@ -293,10 +304,11 @@ def save(self, fname=None): self._surface.write_to_png(fname) return None - fname = fname or self._filename + fname = fname or self._filename if fname is None: - raise ValueError("no file name is known for the surface " + \ - "and none given") + raise ValueError( + "no file name is known for the surface " + "and none given" + ) return self._surface.write_to_png(fname) if fname is not None: @@ -305,12 +317,12 @@ def save(self, fname=None): self._ctx.show_page() self._surface.finish() - def show(self): """Saves the plot to a temporary file and shows it.""" if not isinstance(self._surface, cairo.ImageSurface): - sur = cairo.ImageSurface(cairo.FORMAT_ARGB32, - int(self.bbox.width), int(self.bbox.height)) + sur = cairo.ImageSurface( + cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) + ) ctx = cairo.Context(sur) self.redraw(ctx) else: @@ -327,8 +339,9 @@ def show(self): # No image viewer was given and none was detected. This # should only happen on unknown platforms. plat = platform.system() - raise NotImplementedError("showing plots is not implemented " + \ - "on this platform: %s" % plat) + raise NotImplementedError( + "showing plots is not implemented " + "on this platform: %s" % plat + ) else: os.system("%s %s" % (imgviewer, tmpfile)) if platform.system() == "Darwin" or self._windows_hacks: @@ -357,11 +370,11 @@ def _repr_svg_(self): # Return the raw SVG representation result = io.getvalue() if hasattr(result, "encode"): - result = result.encode("utf-8") # for Python 2.x + result = result.encode("utf-8") # for Python 2.x else: - result = result.decode("utf-8") # for Python 3.x + result = result.decode("utf-8") # for Python 3.x - return result, {'isolated': True} # put it inside an iframe + return result, {"isolated": True} # put it inside an iframe @property def bounding_box(self): @@ -452,7 +465,7 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @see: Graph.__plot__ """ - if hasattr(plt, 'Axes') and isinstance(target, plt.Axes): + if hasattr(plt, "Axes") and isinstance(target, plt.Axes): result = MatplotlibGraphDrawer(ax=target) result.draw(obj, *args, **kwds) return @@ -496,5 +509,5 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): # Also return the plot itself return result -##################################################################### +##################################################################### diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index d06e76e4a..ab2c98dc7 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -2,12 +2,12 @@ Abstract base classes for the drawing routines. """ -from igraph.compat import property from igraph.drawing.utils import BoundingBox from math import pi ##################################################################### + # pylint: disable-msg=R0903 # R0903: too few public methods class AbstractDrawer(object): @@ -18,14 +18,16 @@ def draw(self, *args, **kwds): """Abstract method, must be implemented in derived classes.""" raise NotImplementedError("abstract class") + ##################################################################### + # pylint: disable-msg=R0903 # R0903: too few public methods class AbstractCairoDrawer(AbstractDrawer): """Abstract class that serves as a base class for anything that draws on a Cairo context within a given bounding box. - + A subclass of L{AbstractCairoDrawer} is guaranteed to have an attribute named C{context} that represents the Cairo context to draw on, and an attribute named C{bbox} for the L{BoundingBox} @@ -78,21 +80,22 @@ def _mark_point(self, x, y, color=0, size=4): @param size: the diameter of the marker. """ if isinstance(color, int): - colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), - (0, 1, 1), (1, 0, 1)] + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (0, 1, 1), (1, 0, 1)] color = colors[color % len(colors)] if len(color) == 3: - color += (0.5, ) + color += (0.5,) ctx = self.context ctx.save() ctx.set_source_rgba(*color) - ctx.arc(x, y, size / 2.0, 0, 2*pi) + ctx.arc(x, y, size / 2.0, 0, 2 * pi) ctx.fill() ctx.restore() + ##################################################################### + class AbstractXMLRPCDrawer(AbstractDrawer): """Abstract drawer that uses a remote service via XML-RPC to draw something on a remote display. @@ -101,7 +104,7 @@ class AbstractXMLRPCDrawer(AbstractDrawer): def __init__(self, url, service=None): """Constructs an abstract drawer using the XML-RPC service at the given URL. - + @param url: the URL where the XML-RPC calls for the service should be addressed to. @param service: the name of the service at the XML-RPC address. If @@ -109,9 +112,10 @@ def __init__(self, url, service=None): constructed by C{xmlrpclib.ServerProxy}; if not C{None}, the given attribute will be looked up in the server proxy object. """ - import xmlrpclib + import xmlrpc.client + url = self._resolve_hostname(url) - self.server = xmlrpclib.ServerProxy(url) + self.server = xmlrpc.client.ServerProxy(url) if service is None: self.service = self.server else: @@ -123,7 +127,7 @@ def _resolve_hostname(url): and returns a new URL with the resolved IP address. This speeds up things big time on Mac OS X where an IP lookup would be performed for every XML-RPC call otherwise.""" - from urlparse import urlparse, urlunparse + from urllib.parse import urlparse, urlunparse import re url_parts = urlparse(url) @@ -133,12 +137,12 @@ def _resolve_hostname(url): return url from socket import gethostbyname + if ":" in hostname: - hostname = hostname[0:hostname.index(":")] + hostname = hostname[0 : hostname.index(":")] hostname = gethostbyname(hostname) if url_parts.port is not None: hostname = "%s:%d" % (hostname, url_parts.port) url_parts = list(url_parts) url_parts[1] = hostname return urlunparse(url_parts) - diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index 14fa328e8..c77487667 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -4,7 +4,7 @@ Color handling functions. """ -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -28,12 +28,27 @@ from igraph.utils import str_to_orientation from math import ceil -__all__ = ["Palette", "GradientPalette", "AdvancedGradientPalette", \ - "RainbowPalette", "PrecalculatedPalette", "ClusterColoringPalette", \ - "color_name_to_rgb", "color_name_to_rgba", \ - "hsv_to_rgb", "hsva_to_rgba", "hsl_to_rgb", "hsla_to_rgba", \ - "rgb_to_hsv", "rgba_to_hsva", "rgb_to_hsl", "rgba_to_hsla", \ - "palettes", "known_colors"] +__all__ = [ + "Palette", + "GradientPalette", + "AdvancedGradientPalette", + "RainbowPalette", + "PrecalculatedPalette", + "ClusterColoringPalette", + "color_name_to_rgb", + "color_name_to_rgba", + "hsv_to_rgb", + "hsva_to_rgba", + "hsl_to_rgb", + "hsla_to_rgba", + "rgb_to_hsv", + "rgba_to_hsva", + "rgb_to_hsl", + "rgba_to_hsla", + "palettes", + "known_colors", +] + class Palette(object): """Base class of color palettes. @@ -48,6 +63,7 @@ class Palette(object): Palettes can also be used as lists or dicts, for the C{__getitem__} method is overridden properly to call L{Palette.get}. """ + def __init__(self, n): self._length = n self._cache = {} @@ -88,7 +104,7 @@ def get(self, v): return self._cache[v] except KeyError: pass - if isinstance(v, (int, long)): + if isinstance(v, int): if v < 0: raise IndexError("color index must be non-negative") if v >= self._length: @@ -115,7 +131,7 @@ class will simply try to interpret it as a single color by @return: the colors as a list of RGBA quadruplets. The result will be a list even if you passed a single color index or color name. """ - if isinstance(colors, (basestring, int, long)): + if isinstance(colors, (str, int)): # Single color name or index return [self.get(colors)] # Multiple colors @@ -162,12 +178,12 @@ def __plot__(self, context, bbox, palette, *args, **kwds): C{rl} = C{right-left}, C{tb} = C{top-bottom}, C{bt} = C{bottom-top}. The default is C{left-right}. """ - border_width = float(kwds.get("border_width", 1.)) - grid_width = float(kwds.get("grid_width", 0.)) + border_width = float(kwds.get("border_width", 1.0)) + grid_width = float(kwds.get("grid_width", 0.0)) orientation = str_to_orientation(kwds.get("orientation", "lr")) # Construct a matrix and plot that - indices = range(len(self)) + indices = list(range(len(self))) if orientation in ("rl", "bt"): indices.reverse() if orientation in ("lr", "rl"): @@ -175,9 +191,15 @@ def __plot__(self, context, bbox, palette, *args, **kwds): else: matrix = Matrix([[i] for i in indices]) - return matrix.__plot__(context, bbox, self, style="palette", - square=False, grid_width=grid_width, - border_width=border_width) + return matrix.__plot__( + context, + bbox, + self, + style="palette", + square=False, + grid_width=grid_width, + border_width=border_width, + ) def __repr__(self): return "<%s with %d colors>" % (self.__class__.__name__, self._length) @@ -215,9 +237,10 @@ def _get(self, v): @param v: numerical index of the color to be retrieved @return: a 4-tuple containing the RGBA values""" - ratio = float(v)/(len(self)-1) - return tuple(self._color1[x]*(1-ratio) + \ - self._color2[x]*ratio for x in range(4)) + ratio = float(v) / (len(self) - 1) + return tuple( + self._color1[x] * (1 - ratio) + self._color2[x] * ratio for x in range(4) + ) class AdvancedGradientPalette(Palette): @@ -243,14 +266,15 @@ def __init__(self, colors, indices=None, n=256): Palette.__init__(self, n) if indices is None: - diff = float(n-1) / (len(colors)-1) - indices = [i * diff for i in xrange(len(colors))] + diff = float(n - 1) / (len(colors) - 1) + indices = [i * diff for i in range(len(colors))] elif not hasattr(indices, "__iter__"): indices = [float(x) for x in indices] - self._indices, self._colors = zip(*sorted(zip(indices, colors))) + self._indices, self._colors = list(zip(*sorted(zip(indices, colors)))) self._colors = [color_name_to_rgba(color) for color in self._colors] - self._dists = [curr-prev for curr, prev in \ - zip(self._indices[1:], self._indices)] + self._dists = [ + curr - prev for curr, prev in zip(self._indices[1:], self._indices) + ] def _get(self, v): """Returns the color corresponding to the given color index. @@ -258,13 +282,18 @@ def _get(self, v): @param v: numerical index of the color to be retrieved @return: a 4-tuple containing the RGBA values""" colors = self._colors - for i in xrange(len(self._indices)-1): - if self._indices[i] <= v and self._indices[i+1] >= v: + for i in range(len(self._indices) - 1): + if self._indices[i] <= v and self._indices[i + 1] >= v: dist = self._dists[i] - ratio = float(v-self._indices[i])/dist - return tuple([colors[i][x]*(1-ratio)+colors[i+1][x]*ratio \ - for x in range(4)]) - return (0., 0., 0., 1.) + ratio = float(v - self._indices[i]) / dist + return tuple( + [ + colors[i][x] * (1 - ratio) + colors[i + 1][x] * ratio + for x in range(4) + ] + ) + return (0.0, 0.0, 0.0, 1.0) + class RainbowPalette(Palette): """A palette that varies the hue of the colors along a scale. @@ -324,8 +353,7 @@ def _get(self, v): @param v: numerical index of the color to be retrieved @return: a 4-tuple containing the RGBA values""" - return hsva_to_rgba(self._start + v * self._dh, - self._s, self._v, self._alpha) + return hsva_to_rgba(self._start + v * self._dh, self._s, self._v, self._alpha) class PrecalculatedPalette(Palette): @@ -338,7 +366,7 @@ def __init__(self, l): L{color_name_to_rgba()} is OK here.""" Palette.__init__(self, len(l)) for idx, color in enumerate(l): - if isinstance(color, basestring): + if isinstance(color, str): color = color_name_to_rgba(color) self._cache[idx] = color @@ -365,8 +393,7 @@ class ClusterColoringPalette(PrecalculatedPalette): """ def __init__(self, n): - base_colors = ["red", "green", "blue", "yellow", \ - "magenta", "cyan", "#808080"] + base_colors = ["red", "green", "blue", "yellow", "magenta", "cyan", "#808080"] base_colors = [color_name_to_rgba(name) for name in base_colors] num_base_colors = len(base_colors) @@ -398,6 +425,7 @@ def clamp(value, min_value, max_value): return min_value return value + def color_name_to_rgb(color, palette=None): """Converts a color given in one of the supported color formats to R-G-B values. @@ -410,6 +438,7 @@ def color_name_to_rgb(color, palette=None): """ return color_name_to_rgba(color, palette)[:3] + def color_name_to_rgba(color, palette=None): """Converts a color given in one of the supported color formats to R-G-B-A values. @@ -462,7 +491,7 @@ def color_name_to_rgba(color, palette=None): Since these colors are primarily used by Cairo routines, the tuples contain floats in the range 0.0-1.0 """ - if not isinstance(color, basestring): + if not isinstance(color, str): if hasattr(color, "__iter__"): components = list(color) else: @@ -472,20 +501,20 @@ def color_name_to_rgba(color, palette=None): except AttributeError: raise ValueError("palette index used when no palette was given") if len(components) < 4: - components += [1.] * (4 - len(components)) + components += [1.0] * (4 - len(components)) else: - if color[0] == '#': + if color[0] == "#": color = color[1:] if len(color) == 3: - components = [int(i, 16) * 17. / 255. for i in color] + components = [int(i, 16) * 17.0 / 255.0 for i in color] components.append(1.0) elif len(color) == 4: - components = [int(i, 16) * 17. / 255. for i in color] + components = [int(i, 16) * 17.0 / 255.0 for i in color] elif len(color) == 6: - components = [int(color[i:i+2], 16) / 255. for i in (0, 2, 4)] + components = [int(color[i : i + 2], 16) / 255.0 for i in (0, 2, 4)] components.append(1.0) elif len(color) == 8: - components = [int(color[i:i+2], 16) / 255. for i in (0, 2, 4, 6)] + components = [int(color[i : i + 2], 16) / 255.0 for i in (0, 2, 4, 6)] elif color.lower() in known_colors: components = known_colors[color.lower()] else: @@ -493,7 +522,7 @@ def color_name_to_rgba(color, palette=None): maximums = (255.0, 255.0, 255.0, 1.0) for mode in ["rgb(", "rgba(", "hsv(", "hsva(", "hsl(", "hsla("]: if color.startswith(mode) and color[-1] == ")": - color = color[len(mode):-1] + color = color[len(mode) : -1] color_mode = mode[:-1] if mode[0] == "h": maximums = (360.0, 100.0, 100.0, 1.0) @@ -504,11 +533,11 @@ def color_name_to_rgba(color, palette=None): components = color.split() for idx, comp in enumerate(components): if comp[-1] == "%": - components[idx] = float(comp[:-1])/100. + components[idx] = float(comp[:-1]) / 100.0 else: - components[idx] = float(comp)/maximums[idx] + components[idx] = float(comp) / maximums[idx] if len(components) < 4: - components += [1.] * (4 - len(components)) + components += [1.0] * (4 - len(components)) if color_mode[:3] == "hsv": components = hsva_to_rgba(*components) elif color_mode[:3] == "hsl": @@ -517,7 +546,8 @@ def color_name_to_rgba(color, palette=None): components = palette.get(int(color)) # At this point, the components are floats - return tuple(clamp(val, 0., 1.) for val in components) + return tuple(clamp(val, 0.0, 1.0) for val in components) + def color_to_html_format(color): """Formats a color given as a 3-tuple or 4-tuple in HTML format. @@ -532,6 +562,7 @@ def color_to_html_format(color): return "#{0:02X}{1:02X}{2:02X}{3:02X}".format(*color) return "#{0:02X}{1:02X}{2:02X}".format(*color) + def darken(color, ratio=0.5): """Creates a darker version of a color given by an RGB triplet. @@ -543,7 +574,8 @@ def darken(color, ratio=0.5): red, green, blue, alpha = color return (red * ratio, green * ratio, blue * ratio, alpha) -def hsla_to_rgba(h, s, l, alpha = 1.0): + +def hsla_to_rgba(h, s, l, alpha=1.0): """Converts a color given by its HSLA coordinates (hue, saturation, lightness, alpha) to RGBA coordinates. @@ -551,25 +583,26 @@ def hsla_to_rgba(h, s, l, alpha = 1.0): """ # This is based on the formulae found at: # http://en.wikipedia.org/wiki/HSL_and_HSV - c = s*(1 - 2*abs(l - 0.5)) - h1 = (h*6) % 6 - x = c*(1 - abs(h1 % 2 - 1)) - m = l - c/2. + c = s * (1 - 2 * abs(l - 0.5)) + h1 = (h * 6) % 6 + x = c * (1 - abs(h1 % 2 - 1)) + m = l - c / 2.0 h1 = int(h1) if h1 < 3: if h1 < 1: - return (c+m, x+m, m, alpha) + return (c + m, x + m, m, alpha) elif h1 < 2: - return (x+m, c+m, m, alpha) + return (x + m, c + m, m, alpha) else: - return (m, c+m, x+m, alpha) + return (m, c + m, x + m, alpha) else: if h1 < 4: - return (m, x+m, c+m, alpha) + return (m, x + m, c + m, alpha) elif h1 < 5: - return (x+m, m, c+m, alpha) + return (x + m, m, c + m, alpha) else: - return (c+m, m, x+m, alpha) + return (c + m, m, x + m, alpha) + def hsl_to_rgb(h, s, l): """Converts a color given by its HSL coordinates (hue, saturation, @@ -579,7 +612,8 @@ def hsl_to_rgb(h, s, l): """ return hsla_to_rgba(h, s, l)[:3] -def hsva_to_rgba(h, s, v, alpha = 1.0): + +def hsva_to_rgba(h, s, v, alpha=1.0): """Converts a color given by its HSVA coordinates (hue, saturation, value, alpha) to RGB coordinates. @@ -587,25 +621,26 @@ def hsva_to_rgba(h, s, v, alpha = 1.0): """ # This is based on the formulae found at: # http://en.wikipedia.org/wiki/HSL_and_HSV - c = v*s - h1 = (h*6) % 6 - x = c*(1 - abs(h1 % 2 - 1)) - m = v-c + c = v * s + h1 = (h * 6) % 6 + x = c * (1 - abs(h1 % 2 - 1)) + m = v - c h1 = int(h1) if h1 < 3: if h1 < 1: - return (c+m, x+m, m, alpha) + return (c + m, x + m, m, alpha) elif h1 < 2: - return (x+m, c+m, m, alpha) + return (x + m, c + m, m, alpha) else: - return (m, c+m, x+m, alpha) + return (m, c + m, x + m, alpha) else: if h1 < 4: - return (m, x+m, c+m, alpha) + return (m, x + m, c + m, alpha) elif h1 < 5: - return (x+m, m, c+m, alpha) + return (x + m, m, c + m, alpha) else: - return (c+m, m, x+m, alpha) + return (c + m, m, x + m, alpha) + def hsv_to_rgb(h, s, v): """Converts a color given by its HSV coordinates (hue, saturation, @@ -615,6 +650,7 @@ def hsv_to_rgb(h, s, v): """ return hsva_to_rgba(h, s, v)[:3] + def rgba_to_hsla(r, g, b, alpha=1.0): """Converts a color given by its RGBA coordinates to HSLA coordinates (hue, saturation, lightness, alpha). @@ -640,11 +676,12 @@ def rgba_to_hsla(r, g, b, alpha=1.0): if g < b: hue += 1 elif rgb_max == g: - hue = 1/3.0 + (b - r) / d + hue = 1 / 3.0 + (b - r) / d else: - hue = 2/3.0 + (r - g) / d + hue = 2 / 3.0 + (r - g) / d return hue, sat, lightness, alpha + def rgba_to_hsva(r, g, b, alpha=1.0): """Converts a color given by its RGBA coordinates to HSVA coordinates (hue, saturation, value, alpha). @@ -671,11 +708,12 @@ def rgba_to_hsva(r, g, b, alpha=1.0): if hue < 0: hue += 1 elif rgb_max == g: - hue = 1/3.0 + (b - r) / 6.0 + hue = 1 / 3.0 + (b - r) / 6.0 else: - hue = 2/3.0 + (r - g) / 6.0 + hue = 2 / 3.0 + (r - g) / 6.0 return hue, sat, value, alpha + def rgb_to_hsl(r, g, b): """Converts a color given by its RGB coordinates to HSL coordinates (hue, saturation, lightness). @@ -684,6 +722,7 @@ def rgb_to_hsl(r, g, b): """ return rgba_to_hsla(r, g, b)[:3] + def rgb_to_hsv(r, g, b): """Converts a color given by its RGB coordinates to HSV coordinates (hue, saturation, value). @@ -692,6 +731,7 @@ def rgb_to_hsv(r, g, b): """ return rgba_to_hsva(r, g, b)[:3] + def lighten(color, ratio=0.5): """Creates a lighter version of a color given by an RGB triplet. @@ -700,2406 +740,1314 @@ def lighten(color, ratio=0.5): of 0.0 will yield the original color. """ red, green, blue, alpha = color - return (red + (1.0 - red) * ratio, green + (1.0 - green) * ratio, - blue + (1.0 - blue) * ratio, alpha) - -known_colors = \ -{ 'alice blue': (0.94117647058823528, 0.97254901960784312, 1.0, 1.0), - 'aliceblue': (0.94117647058823528, 0.97254901960784312, 1.0, 1.0), - 'antique white': ( 0.98039215686274506, - 0.92156862745098034, - 0.84313725490196079, - 1.0), - 'antiquewhite': ( 0.98039215686274506, - 0.92156862745098034, - 0.84313725490196079, - 1.0), - 'antiquewhite1': (1.0, 0.93725490196078431, 0.85882352941176465, 1.0), - 'antiquewhite2': ( 0.93333333333333335, - 0.87450980392156863, - 0.80000000000000004, - 1.0), - 'antiquewhite3': ( 0.80392156862745101, - 0.75294117647058822, - 0.69019607843137254, - 1.0), - 'antiquewhite4': ( 0.54509803921568623, - 0.51372549019607838, - 0.47058823529411764, - 1.0), - 'aqua': (0.0, 1.0, 1.0, 1.0), - 'aquamarine': (0.49803921568627452, 1.0, 0.83137254901960789, 1.0), - 'aquamarine1': (0.49803921568627452, 1.0, 0.83137254901960789, 1.0), - 'aquamarine2': ( 0.46274509803921571, - 0.93333333333333335, - 0.77647058823529413, - 1.0), - 'aquamarine3': ( 0.40000000000000002, - 0.80392156862745101, - 0.66666666666666663, - 1.0), - 'aquamarine4': ( 0.27058823529411763, - 0.54509803921568623, - 0.45490196078431372, - 1.0), - 'azure': (0.94117647058823528, 1.0, 1.0, 1.0), - 'azure1': (0.94117647058823528, 1.0, 1.0, 1.0), - 'azure2': ( 0.8784313725490196, - 0.93333333333333335, - 0.93333333333333335, - 1.0), - 'azure3': ( 0.75686274509803919, - 0.80392156862745101, - 0.80392156862745101, - 1.0), - 'azure4': ( 0.51372549019607838, - 0.54509803921568623, - 0.54509803921568623, - 1.0), - 'beige': ( 0.96078431372549022, - 0.96078431372549022, - 0.86274509803921573, - 1.0), - 'bisque': (1.0, 0.89411764705882357, 0.7686274509803922, 1.0), - 'bisque1': (1.0, 0.89411764705882357, 0.7686274509803922, 1.0), - 'bisque2': ( 0.93333333333333335, - 0.83529411764705885, - 0.71764705882352942, - 1.0), - 'bisque3': ( 0.80392156862745101, - 0.71764705882352942, - 0.61960784313725492, - 1.0), - 'bisque4': ( 0.54509803921568623, - 0.49019607843137253, - 0.41960784313725491, - 1.0), - 'black': (0.0, 0.0, 0.0, 1.0), - 'blanched almond': (1.0, 0.92156862745098034, 0.80392156862745101, 1.0), - 'blanchedalmond': (1.0, 0.92156862745098034, 0.80392156862745101, 1.0), - 'blue': (0.0, 0.0, 1.0, 1.0), - 'blue violet': ( 0.54117647058823526, - 0.16862745098039217, - 0.88627450980392153, - 1.0), - 'blue1': (0.0, 0.0, 1.0, 1.0), - 'blue2': (0.0, 0.0, 0.93333333333333335, 1.0), - 'blue3': (0.0, 0.0, 0.80392156862745101, 1.0), - 'blue4': (0.0, 0.0, 0.54509803921568623, 1.0), - 'blueviolet': ( 0.54117647058823526, - 0.16862745098039217, - 0.88627450980392153, - 1.0), - 'brown': ( 0.6470588235294118, - 0.16470588235294117, - 0.16470588235294117, - 1.0), - 'brown1': (1.0, 0.25098039215686274, 0.25098039215686274, 1.0), - 'brown2': ( 0.93333333333333335, - 0.23137254901960785, - 0.23137254901960785, - 1.0), - 'brown3': ( 0.80392156862745101, - 0.20000000000000001, - 0.20000000000000001, - 1.0), - 'brown4': ( 0.54509803921568623, - 0.13725490196078433, - 0.13725490196078433, - 1.0), - 'burlywood': ( 0.87058823529411766, - 0.72156862745098038, - 0.52941176470588236, - 1.0), - 'burlywood1': (1.0, 0.82745098039215681, 0.60784313725490191, 1.0), - 'burlywood2': ( 0.93333333333333335, - 0.77254901960784317, - 0.56862745098039214, - 1.0), - 'burlywood3': ( 0.80392156862745101, - 0.66666666666666663, - 0.49019607843137253, - 1.0), - 'burlywood4': ( 0.54509803921568623, - 0.45098039215686275, - 0.33333333333333331, - 1.0), - 'cadet blue': ( 0.37254901960784315, - 0.61960784313725492, - 0.62745098039215685, - 1.0), - 'cadetblue': ( 0.37254901960784315, - 0.61960784313725492, - 0.62745098039215685, - 1.0), - 'cadetblue1': (0.59607843137254901, 0.96078431372549022, 1.0, 1.0), - 'cadetblue2': ( 0.55686274509803924, - 0.89803921568627454, - 0.93333333333333335, - 1.0), - 'cadetblue3': ( 0.47843137254901963, - 0.77254901960784317, - 0.80392156862745101, - 1.0), - 'cadetblue4': ( 0.32549019607843138, - 0.52549019607843139, - 0.54509803921568623, - 1.0), - 'chartreuse': (0.49803921568627452, 1.0, 0.0, 1.0), - 'chartreuse1': (0.49803921568627452, 1.0, 0.0, 1.0), - 'chartreuse2': (0.46274509803921571, 0.93333333333333335, 0.0, 1.0), - 'chartreuse3': (0.40000000000000002, 0.80392156862745101, 0.0, 1.0), - 'chartreuse4': (0.27058823529411763, 0.54509803921568623, 0.0, 1.0), - 'chocolate': ( 0.82352941176470584, - 0.41176470588235292, - 0.11764705882352941, - 1.0), - 'chocolate1': (1.0, 0.49803921568627452, 0.14117647058823529, 1.0), - 'chocolate2': ( 0.93333333333333335, - 0.46274509803921571, - 0.12941176470588237, - 1.0), - 'chocolate3': ( 0.80392156862745101, - 0.40000000000000002, - 0.11372549019607843, - 1.0), - 'chocolate4': ( 0.54509803921568623, - 0.27058823529411763, - 0.074509803921568626, - 1.0), - 'coral': (1.0, 0.49803921568627452, 0.31372549019607843, 1.0), - 'coral1': (1.0, 0.44705882352941179, 0.33725490196078434, 1.0), - 'coral2': ( 0.93333333333333335, - 0.41568627450980394, - 0.31372549019607843, - 1.0), - 'coral3': ( 0.80392156862745101, - 0.35686274509803922, - 0.27058823529411763, - 1.0), - 'coral4': ( 0.54509803921568623, - 0.24313725490196078, - 0.18431372549019609, - 1.0), - 'cornflower blue': ( 0.39215686274509803, - 0.58431372549019611, - 0.92941176470588238, - 1.0), - 'cornflowerblue': ( 0.39215686274509803, - 0.58431372549019611, - 0.92941176470588238, - 1.0), - 'cornsilk': (1.0, 0.97254901960784312, 0.86274509803921573, 1.0), - 'cornsilk1': (1.0, 0.97254901960784312, 0.86274509803921573, 1.0), - 'cornsilk2': ( 0.93333333333333335, - 0.90980392156862744, - 0.80392156862745101, - 1.0), - 'cornsilk3': ( 0.80392156862745101, - 0.78431372549019607, - 0.69411764705882351, - 1.0), - 'cornsilk4': ( 0.54509803921568623, - 0.53333333333333333, - 0.47058823529411764, - 1.0), - 'crimson': ( 0.8627450980392157, - 0.0784313725490196, - 0.23529411764705882, - 1.0), - 'cyan': (0.0, 1.0, 1.0, 1.0), - 'cyan1': (0.0, 1.0, 1.0, 1.0), - 'cyan2': (0.0, 0.93333333333333335, 0.93333333333333335, 1.0), - 'cyan3': (0.0, 0.80392156862745101, 0.80392156862745101, 1.0), - 'cyan4': (0.0, 0.54509803921568623, 0.54509803921568623, 1.0), - 'dark blue': (0.0, 0.0, 0.54509803921568623, 1.0), - 'dark cyan': (0.0, 0.54509803921568623, 0.54509803921568623, 1.0), - 'dark goldenrod': ( 0.72156862745098038, - 0.52549019607843139, - 0.043137254901960784, - 1.0), - 'dark gray': ( 0.66274509803921566, - 0.66274509803921566, - 0.66274509803921566, - 1.0), - 'dark green': (0.0, 0.39215686274509803, 0.0, 1.0), - 'dark grey': ( 0.66274509803921566, - 0.66274509803921566, - 0.66274509803921566, - 1.0), - 'dark khaki': ( 0.74117647058823533, - 0.71764705882352942, - 0.41960784313725491, - 1.0), - 'dark magenta': (0.54509803921568623, 0.0, 0.54509803921568623, 1.0), - 'dark olive green': ( 0.33333333333333331, - 0.41960784313725491, - 0.18431372549019609, - 1.0), - 'dark orange': (1.0, 0.5490196078431373, 0.0, 1.0), - 'dark orchid': ( 0.59999999999999998, - 0.19607843137254902, - 0.80000000000000004, - 1.0), - 'dark red': (0.54509803921568623, 0.0, 0.0, 1.0), - 'dark salmon': ( 0.9137254901960784, - 0.58823529411764708, - 0.47843137254901963, - 1.0), - 'dark sea green': ( 0.5607843137254902, - 0.73725490196078436, - 0.5607843137254902, - 1.0), - 'dark slate blue': ( 0.28235294117647058, - 0.23921568627450981, - 0.54509803921568623, - 1.0), - 'dark slate gray': ( 0.18431372549019609, - 0.30980392156862746, - 0.30980392156862746, - 1.0), - 'dark slate grey': ( 0.18431372549019609, - 0.30980392156862746, - 0.30980392156862746, - 1.0), - 'dark turquoise': (0.0, 0.80784313725490198, 0.81960784313725488, 1.0), - 'dark violet': (0.58039215686274515, 0.0, 0.82745098039215681, 1.0), - 'darkblue': (0.0, 0.0, 0.54509803921568623, 1.0), - 'darkcyan': (0.0, 0.54509803921568623, 0.54509803921568623, 1.0), - 'darkgoldenrod': ( 0.72156862745098038, - 0.52549019607843139, - 0.043137254901960784, - 1.0), - 'darkgoldenrod1': (1.0, 0.72549019607843135, 0.058823529411764705, 1.0), - 'darkgoldenrod2': ( 0.93333333333333335, - 0.67843137254901964, - 0.054901960784313725, - 1.0), - 'darkgoldenrod3': ( 0.80392156862745101, - 0.58431372549019611, - 0.047058823529411764, - 1.0), - 'darkgoldenrod4': ( 0.54509803921568623, - 0.396078431372549, - 0.031372549019607843, - 1.0), - 'darkgray': ( 0.66274509803921566, - 0.66274509803921566, - 0.66274509803921566, - 1.0), - 'darkgreen': (0.0, 0.39215686274509803, 0.0, 1.0), - 'darkgrey': ( 0.66274509803921566, - 0.66274509803921566, - 0.66274509803921566, - 1.0), - 'darkkhaki': ( 0.74117647058823533, - 0.71764705882352942, - 0.41960784313725491, - 1.0), - 'darkmagenta': (0.54509803921568623, 0.0, 0.54509803921568623, 1.0), - 'darkolivegreen': ( 0.33333333333333331, - 0.41960784313725491, - 0.18431372549019609, - 1.0), - 'darkolivegreen1': (0.792156862745098, 1.0, 0.4392156862745098, 1.0), - 'darkolivegreen2': ( 0.73725490196078436, - 0.93333333333333335, - 0.40784313725490196, - 1.0), - 'darkolivegreen3': ( 0.63529411764705879, - 0.80392156862745101, - 0.35294117647058826, - 1.0), - 'darkolivegreen4': ( 0.43137254901960786, - 0.54509803921568623, - 0.23921568627450981, - 1.0), - 'darkorange': (1.0, 0.5490196078431373, 0.0, 1.0), - 'darkorange1': (1.0, 0.49803921568627452, 0.0, 1.0), - 'darkorange2': (0.93333333333333335, 0.46274509803921571, 0.0, 1.0), - 'darkorange3': (0.80392156862745101, 0.40000000000000002, 0.0, 1.0), - 'darkorange4': (0.54509803921568623, 0.27058823529411763, 0.0, 1.0), - 'darkorchid': ( 0.59999999999999998, - 0.19607843137254902, - 0.80000000000000004, - 1.0), - 'darkorchid1': (0.74901960784313726, 0.24313725490196078, 1.0, 1.0), - 'darkorchid2': ( 0.69803921568627447, - 0.22745098039215686, - 0.93333333333333335, - 1.0), - 'darkorchid3': ( 0.60392156862745094, - 0.19607843137254902, - 0.80392156862745101, - 1.0), - 'darkorchid4': ( 0.40784313725490196, - 0.13333333333333333, - 0.54509803921568623, - 1.0), - 'darkred': (0.54509803921568623, 0.0, 0.0, 1.0), - 'darksalmon': ( 0.9137254901960784, - 0.58823529411764708, - 0.47843137254901963, - 1.0), - 'darkseagreen': ( 0.5607843137254902, - 0.73725490196078436, - 0.5607843137254902, - 1.0), - 'darkseagreen1': (0.75686274509803919, 1.0, 0.75686274509803919, 1.0), - 'darkseagreen2': ( 0.70588235294117652, - 0.93333333333333335, - 0.70588235294117652, - 1.0), - 'darkseagreen3': ( 0.60784313725490191, - 0.80392156862745101, - 0.60784313725490191, - 1.0), - 'darkseagreen4': ( 0.41176470588235292, - 0.54509803921568623, - 0.41176470588235292, - 1.0), - 'darkslateblue': ( 0.28235294117647058, - 0.23921568627450981, - 0.54509803921568623, - 1.0), - 'darkslategray': ( 0.18431372549019609, - 0.30980392156862746, - 0.30980392156862746, - 1.0), - 'darkslategray1': (0.59215686274509804, 1.0, 1.0, 1.0), - 'darkslategray2': ( 0.55294117647058827, - 0.93333333333333335, - 0.93333333333333335, - 1.0), - 'darkslategray3': ( 0.47450980392156861, - 0.80392156862745101, - 0.80392156862745101, - 1.0), - 'darkslategray4': ( 0.32156862745098042, - 0.54509803921568623, - 0.54509803921568623, - 1.0), - 'darkslategrey': ( 0.18431372549019609, - 0.30980392156862746, - 0.30980392156862746, - 1.0), - 'darkturquoise': (0.0, 0.80784313725490198, 0.81960784313725488, 1.0), - 'darkviolet': (0.58039215686274515, 0.0, 0.82745098039215681, 1.0), - 'deep pink': (1.0, 0.078431372549019607, 0.57647058823529407, 1.0), - 'deep sky blue': (0.0, 0.74901960784313726, 1.0, 1.0), - 'deeppink': (1.0, 0.078431372549019607, 0.57647058823529407, 1.0), - 'deeppink1': (1.0, 0.078431372549019607, 0.57647058823529407, 1.0), - 'deeppink2': ( 0.93333333333333335, - 0.070588235294117646, - 0.53725490196078429, - 1.0), - 'deeppink3': ( 0.80392156862745101, - 0.062745098039215685, - 0.46274509803921571, - 1.0), - 'deeppink4': ( 0.54509803921568623, - 0.039215686274509803, - 0.31372549019607843, - 1.0), - 'deepskyblue': (0.0, 0.74901960784313726, 1.0, 1.0), - 'deepskyblue1': (0.0, 0.74901960784313726, 1.0, 1.0), - 'deepskyblue2': (0.0, 0.69803921568627447, 0.93333333333333335, 1.0), - 'deepskyblue3': (0.0, 0.60392156862745094, 0.80392156862745101, 1.0), - 'deepskyblue4': (0.0, 0.40784313725490196, 0.54509803921568623, 1.0), - 'dim gray': ( 0.41176470588235292, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'dim grey': ( 0.41176470588235292, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'dimgray': ( 0.41176470588235292, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'dimgrey': ( 0.41176470588235292, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'dodger blue': (0.11764705882352941, 0.56470588235294117, 1.0, 1.0), - 'dodgerblue': (0.11764705882352941, 0.56470588235294117, 1.0, 1.0), - 'dodgerblue1': (0.11764705882352941, 0.56470588235294117, 1.0, 1.0), - 'dodgerblue2': ( 0.10980392156862745, - 0.52549019607843139, - 0.93333333333333335, - 1.0), - 'dodgerblue3': ( 0.094117647058823528, - 0.45490196078431372, - 0.80392156862745101, - 1.0), - 'dodgerblue4': ( 0.062745098039215685, - 0.30588235294117649, - 0.54509803921568623, - 1.0), - 'firebrick': ( 0.69803921568627447, - 0.13333333333333333, - 0.13333333333333333, - 1.0), - 'firebrick1': (1.0, 0.18823529411764706, 0.18823529411764706, 1.0), - 'firebrick2': ( 0.93333333333333335, - 0.17254901960784313, - 0.17254901960784313, - 1.0), - 'firebrick3': ( 0.80392156862745101, - 0.14901960784313725, - 0.14901960784313725, - 1.0), - 'firebrick4': ( 0.54509803921568623, - 0.10196078431372549, - 0.10196078431372549, - 1.0), - 'floral white': (1.0, 0.98039215686274506, 0.94117647058823528, 1.0), - 'floralwhite': (1.0, 0.98039215686274506, 0.94117647058823528, 1.0), - 'forest green': ( 0.13333333333333333, - 0.54509803921568623, - 0.13333333333333333, - 1.0), - 'forestgreen': ( 0.13333333333333333, - 0.54509803921568623, - 0.13333333333333333, - 1.0), - 'fuchsia': (1.0, 0.0, 1.0, 1.0), - 'gainsboro': ( 0.86274509803921573, - 0.86274509803921573, - 0.86274509803921573, - 1.0), - 'ghost white': (0.97254901960784312, 0.97254901960784312, 1.0, 1.0), - 'ghostwhite': (0.97254901960784312, 0.97254901960784312, 1.0, 1.0), - 'gold': (1.0, 0.84313725490196079, 0.0, 1.0), - 'gold1': (1.0, 0.84313725490196079, 0.0, 1.0), - 'gold2': (0.93333333333333335, 0.78823529411764703, 0.0, 1.0), - 'gold3': (0.80392156862745101, 0.67843137254901964, 0.0, 1.0), - 'gold4': (0.54509803921568623, 0.45882352941176469, 0.0, 1.0), - 'goldenrod': ( 0.85490196078431369, - 0.6470588235294118, - 0.12549019607843137, - 1.0), - 'goldenrod1': (1.0, 0.75686274509803919, 0.14509803921568629, 1.0), - 'goldenrod2': ( 0.93333333333333335, - 0.70588235294117652, - 0.13333333333333333, - 1.0), - 'goldenrod3': ( 0.80392156862745101, - 0.60784313725490191, - 0.11372549019607843, - 1.0), - 'goldenrod4': ( 0.54509803921568623, - 0.41176470588235292, - 0.078431372549019607, - 1.0), - 'gray': ( 0.74509803921568629, - 0.74509803921568629, - 0.74509803921568629, - 1.0), - 'gray0': (0.0, 0.0, 0.0, 1.0), - 'gray1': ( 0.011764705882352941, - 0.011764705882352941, - 0.011764705882352941, - 1.0), - 'gray10': ( 0.10196078431372549, - 0.10196078431372549, - 0.10196078431372549, - 1.0), - 'gray100': (1.0, 1.0, 1.0, 1.0), - 'gray11': ( 0.10980392156862745, - 0.10980392156862745, - 0.10980392156862745, - 1.0), - 'gray12': ( 0.12156862745098039, - 0.12156862745098039, - 0.12156862745098039, - 1.0), - 'gray13': ( 0.12941176470588237, - 0.12941176470588237, - 0.12941176470588237, - 1.0), - 'gray14': ( 0.14117647058823529, - 0.14117647058823529, - 0.14117647058823529, - 1.0), - 'gray15': ( 0.14901960784313725, - 0.14901960784313725, - 0.14901960784313725, - 1.0), - 'gray16': ( 0.16078431372549021, - 0.16078431372549021, - 0.16078431372549021, - 1.0), - 'gray17': ( 0.16862745098039217, - 0.16862745098039217, - 0.16862745098039217, - 1.0), - 'gray18': ( 0.1803921568627451, - 0.1803921568627451, - 0.1803921568627451, - 1.0), - 'gray19': ( 0.18823529411764706, - 0.18823529411764706, - 0.18823529411764706, - 1.0), - 'gray2': ( 0.019607843137254902, - 0.019607843137254902, - 0.019607843137254902, - 1.0), - 'gray20': ( 0.20000000000000001, - 0.20000000000000001, - 0.20000000000000001, - 1.0), - 'gray21': ( 0.21176470588235294, - 0.21176470588235294, - 0.21176470588235294, - 1.0), - 'gray22': ( 0.2196078431372549, - 0.2196078431372549, - 0.2196078431372549, - 1.0), - 'gray23': ( 0.23137254901960785, - 0.23137254901960785, - 0.23137254901960785, - 1.0), - 'gray24': ( 0.23921568627450981, - 0.23921568627450981, - 0.23921568627450981, - 1.0), - 'gray25': ( 0.25098039215686274, - 0.25098039215686274, - 0.25098039215686274, - 1.0), - 'gray26': ( 0.25882352941176473, - 0.25882352941176473, - 0.25882352941176473, - 1.0), - 'gray27': ( 0.27058823529411763, - 0.27058823529411763, - 0.27058823529411763, - 1.0), - 'gray28': ( 0.27843137254901962, - 0.27843137254901962, - 0.27843137254901962, - 1.0), - 'gray29': ( 0.29019607843137257, - 0.29019607843137257, - 0.29019607843137257, - 1.0), - 'gray3': ( 0.031372549019607843, - 0.031372549019607843, - 0.031372549019607843, - 1.0), - 'gray30': ( 0.30196078431372547, - 0.30196078431372547, - 0.30196078431372547, - 1.0), - 'gray31': ( 0.30980392156862746, - 0.30980392156862746, - 0.30980392156862746, - 1.0), - 'gray32': ( 0.32156862745098042, - 0.32156862745098042, - 0.32156862745098042, - 1.0), - 'gray33': ( 0.32941176470588235, - 0.32941176470588235, - 0.32941176470588235, - 1.0), - 'gray34': ( 0.3411764705882353, - 0.3411764705882353, - 0.3411764705882353, - 1.0), - 'gray35': ( 0.34901960784313724, - 0.34901960784313724, - 0.34901960784313724, - 1.0), - 'gray36': ( 0.36078431372549019, - 0.36078431372549019, - 0.36078431372549019, - 1.0), - 'gray37': ( 0.36862745098039218, - 0.36862745098039218, - 0.36862745098039218, - 1.0), - 'gray38': ( 0.38039215686274508, - 0.38039215686274508, - 0.38039215686274508, - 1.0), - 'gray39': ( 0.38823529411764707, - 0.38823529411764707, - 0.38823529411764707, - 1.0), - 'gray4': ( 0.039215686274509803, - 0.039215686274509803, - 0.039215686274509803, - 1.0), - 'gray40': ( 0.40000000000000002, - 0.40000000000000002, - 0.40000000000000002, - 1.0), - 'gray41': ( 0.41176470588235292, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'gray42': ( 0.41960784313725491, - 0.41960784313725491, - 0.41960784313725491, - 1.0), - 'gray43': ( 0.43137254901960786, - 0.43137254901960786, - 0.43137254901960786, - 1.0), - 'gray44': ( 0.4392156862745098, - 0.4392156862745098, - 0.4392156862745098, - 1.0), - 'gray45': ( 0.45098039215686275, - 0.45098039215686275, - 0.45098039215686275, - 1.0), - 'gray46': ( 0.45882352941176469, - 0.45882352941176469, - 0.45882352941176469, - 1.0), - 'gray47': ( 0.47058823529411764, - 0.47058823529411764, - 0.47058823529411764, - 1.0), - 'gray48': ( 0.47843137254901963, - 0.47843137254901963, - 0.47843137254901963, - 1.0), - 'gray49': ( 0.49019607843137253, - 0.49019607843137253, - 0.49019607843137253, - 1.0), - 'gray5': ( 0.050980392156862744, - 0.050980392156862744, - 0.050980392156862744, - 1.0), - 'gray50': ( 0.49803921568627452, - 0.49803921568627452, - 0.49803921568627452, - 1.0), - 'gray51': ( 0.50980392156862742, - 0.50980392156862742, - 0.50980392156862742, - 1.0), - 'gray52': ( 0.52156862745098043, - 0.52156862745098043, - 0.52156862745098043, - 1.0), - 'gray53': ( 0.52941176470588236, - 0.52941176470588236, - 0.52941176470588236, - 1.0), - 'gray54': ( 0.54117647058823526, - 0.54117647058823526, - 0.54117647058823526, - 1.0), - 'gray55': ( 0.5490196078431373, - 0.5490196078431373, - 0.5490196078431373, - 1.0), - 'gray56': ( 0.5607843137254902, - 0.5607843137254902, - 0.5607843137254902, - 1.0), - 'gray57': ( 0.56862745098039214, - 0.56862745098039214, - 0.56862745098039214, - 1.0), - 'gray58': ( 0.58039215686274515, - 0.58039215686274515, - 0.58039215686274515, - 1.0), - 'gray59': ( 0.58823529411764708, - 0.58823529411764708, - 0.58823529411764708, - 1.0), - 'gray6': ( 0.058823529411764705, - 0.058823529411764705, - 0.058823529411764705, - 1.0), - 'gray60': ( 0.59999999999999998, - 0.59999999999999998, - 0.59999999999999998, - 1.0), - 'gray61': ( 0.61176470588235299, - 0.61176470588235299, - 0.61176470588235299, - 1.0), - 'gray62': ( 0.61960784313725492, - 0.61960784313725492, - 0.61960784313725492, - 1.0), - 'gray63': ( 0.63137254901960782, - 0.63137254901960782, - 0.63137254901960782, - 1.0), - 'gray64': ( 0.63921568627450975, - 0.63921568627450975, - 0.63921568627450975, - 1.0), - 'gray65': ( 0.65098039215686276, - 0.65098039215686276, - 0.65098039215686276, - 1.0), - 'gray66': ( 0.6588235294117647, - 0.6588235294117647, - 0.6588235294117647, - 1.0), - 'gray67': ( 0.6705882352941176, - 0.6705882352941176, - 0.6705882352941176, - 1.0), - 'gray68': ( 0.67843137254901964, - 0.67843137254901964, - 0.67843137254901964, - 1.0), - 'gray69': ( 0.69019607843137254, - 0.69019607843137254, - 0.69019607843137254, - 1.0), - 'gray7': ( 0.070588235294117646, - 0.070588235294117646, - 0.070588235294117646, - 1.0), - 'gray70': ( 0.70196078431372544, - 0.70196078431372544, - 0.70196078431372544, - 1.0), - 'gray71': ( 0.70980392156862748, - 0.70980392156862748, - 0.70980392156862748, - 1.0), - 'gray72': ( 0.72156862745098038, - 0.72156862745098038, - 0.72156862745098038, - 1.0), - 'gray73': ( 0.72941176470588232, - 0.72941176470588232, - 0.72941176470588232, - 1.0), - 'gray74': ( 0.74117647058823533, - 0.74117647058823533, - 0.74117647058823533, - 1.0), - 'gray75': ( 0.74901960784313726, - 0.74901960784313726, - 0.74901960784313726, - 1.0), - 'gray76': ( 0.76078431372549016, - 0.76078431372549016, - 0.76078431372549016, - 1.0), - 'gray77': ( 0.7686274509803922, - 0.7686274509803922, - 0.7686274509803922, - 1.0), - 'gray78': ( 0.7803921568627451, - 0.7803921568627451, - 0.7803921568627451, - 1.0), - 'gray79': ( 0.78823529411764703, - 0.78823529411764703, - 0.78823529411764703, - 1.0), - 'gray8': ( 0.078431372549019607, - 0.078431372549019607, - 0.078431372549019607, - 1.0), - 'gray80': ( 0.80000000000000004, - 0.80000000000000004, - 0.80000000000000004, - 1.0), - 'gray81': ( 0.81176470588235294, - 0.81176470588235294, - 0.81176470588235294, - 1.0), - 'gray82': ( 0.81960784313725488, - 0.81960784313725488, - 0.81960784313725488, - 1.0), - 'gray83': ( 0.83137254901960789, - 0.83137254901960789, - 0.83137254901960789, - 1.0), - 'gray84': ( 0.83921568627450982, - 0.83921568627450982, - 0.83921568627450982, - 1.0), - 'gray85': ( 0.85098039215686272, - 0.85098039215686272, - 0.85098039215686272, - 1.0), - 'gray86': ( 0.85882352941176465, - 0.85882352941176465, - 0.85882352941176465, - 1.0), - 'gray87': ( 0.87058823529411766, - 0.87058823529411766, - 0.87058823529411766, - 1.0), - 'gray88': ( 0.8784313725490196, - 0.8784313725490196, - 0.8784313725490196, - 1.0), - 'gray89': ( 0.8901960784313725, - 0.8901960784313725, - 0.8901960784313725, - 1.0), - 'gray9': ( 0.090196078431372548, - 0.090196078431372548, - 0.090196078431372548, - 1.0), - 'gray90': ( 0.89803921568627454, - 0.89803921568627454, - 0.89803921568627454, - 1.0), - 'gray91': ( 0.90980392156862744, - 0.90980392156862744, - 0.90980392156862744, - 1.0), - 'gray92': ( 0.92156862745098034, - 0.92156862745098034, - 0.92156862745098034, - 1.0), - 'gray93': ( 0.92941176470588238, - 0.92941176470588238, - 0.92941176470588238, - 1.0), - 'gray94': ( 0.94117647058823528, - 0.94117647058823528, - 0.94117647058823528, - 1.0), - 'gray95': ( 0.94901960784313721, - 0.94901960784313721, - 0.94901960784313721, - 1.0), - 'gray96': ( 0.96078431372549022, - 0.96078431372549022, - 0.96078431372549022, - 1.0), - 'gray97': ( 0.96862745098039216, - 0.96862745098039216, - 0.96862745098039216, - 1.0), - 'gray98': ( 0.98039215686274506, - 0.98039215686274506, - 0.98039215686274506, - 1.0), - 'gray99': ( 0.9882352941176471, - 0.9882352941176471, - 0.9882352941176471, - 1.0), - 'green': (0.0, 1.0, 0.0, 1.0), - 'green yellow': (0.67843137254901964, 1.0, 0.18431372549019609, 1.0), - 'green1': (0.0, 1.0, 0.0, 1.0), - 'green2': (0.0, 0.93333333333333335, 0.0, 1.0), - 'green3': (0.0, 0.80392156862745101, 0.0, 1.0), - 'green4': (0.0, 0.54509803921568623, 0.0, 1.0), - 'greenyellow': (0.67843137254901964, 1.0, 0.18431372549019609, 1.0), - 'grey': ( 0.74509803921568629, - 0.74509803921568629, - 0.74509803921568629, - 1.0), - 'grey0': (0.0, 0.0, 0.0, 1.0), - 'grey1': ( 0.011764705882352941, - 0.011764705882352941, - 0.011764705882352941, - 1.0), - 'grey10': ( 0.10196078431372549, - 0.10196078431372549, - 0.10196078431372549, - 1.0), - 'grey100': (1.0, 1.0, 1.0, 1.0), - 'grey11': ( 0.10980392156862745, - 0.10980392156862745, - 0.10980392156862745, - 1.0), - 'grey12': ( 0.12156862745098039, - 0.12156862745098039, - 0.12156862745098039, - 1.0), - 'grey13': ( 0.12941176470588237, - 0.12941176470588237, - 0.12941176470588237, - 1.0), - 'grey14': ( 0.14117647058823529, - 0.14117647058823529, - 0.14117647058823529, - 1.0), - 'grey15': ( 0.14901960784313725, - 0.14901960784313725, - 0.14901960784313725, - 1.0), - 'grey16': ( 0.16078431372549021, - 0.16078431372549021, - 0.16078431372549021, - 1.0), - 'grey17': ( 0.16862745098039217, - 0.16862745098039217, - 0.16862745098039217, - 1.0), - 'grey18': ( 0.1803921568627451, - 0.1803921568627451, - 0.1803921568627451, - 1.0), - 'grey19': ( 0.18823529411764706, - 0.18823529411764706, - 0.18823529411764706, - 1.0), - 'grey2': ( 0.019607843137254902, - 0.019607843137254902, - 0.019607843137254902, - 1.0), - 'grey20': ( 0.20000000000000001, - 0.20000000000000001, - 0.20000000000000001, - 1.0), - 'grey21': ( 0.21176470588235294, - 0.21176470588235294, - 0.21176470588235294, - 1.0), - 'grey22': ( 0.2196078431372549, - 0.2196078431372549, - 0.2196078431372549, - 1.0), - 'grey23': ( 0.23137254901960785, - 0.23137254901960785, - 0.23137254901960785, - 1.0), - 'grey24': ( 0.23921568627450981, - 0.23921568627450981, - 0.23921568627450981, - 1.0), - 'grey25': ( 0.25098039215686274, - 0.25098039215686274, - 0.25098039215686274, - 1.0), - 'grey26': ( 0.25882352941176473, - 0.25882352941176473, - 0.25882352941176473, - 1.0), - 'grey27': ( 0.27058823529411763, - 0.27058823529411763, - 0.27058823529411763, - 1.0), - 'grey28': ( 0.27843137254901962, - 0.27843137254901962, - 0.27843137254901962, - 1.0), - 'grey29': ( 0.29019607843137257, - 0.29019607843137257, - 0.29019607843137257, - 1.0), - 'grey3': ( 0.031372549019607843, - 0.031372549019607843, - 0.031372549019607843, - 1.0), - 'grey30': ( 0.30196078431372547, - 0.30196078431372547, - 0.30196078431372547, - 1.0), - 'grey31': ( 0.30980392156862746, - 0.30980392156862746, - 0.30980392156862746, - 1.0), - 'grey32': ( 0.32156862745098042, - 0.32156862745098042, - 0.32156862745098042, - 1.0), - 'grey33': ( 0.32941176470588235, - 0.32941176470588235, - 0.32941176470588235, - 1.0), - 'grey34': ( 0.3411764705882353, - 0.3411764705882353, - 0.3411764705882353, - 1.0), - 'grey35': ( 0.34901960784313724, - 0.34901960784313724, - 0.34901960784313724, - 1.0), - 'grey36': ( 0.36078431372549019, - 0.36078431372549019, - 0.36078431372549019, - 1.0), - 'grey37': ( 0.36862745098039218, - 0.36862745098039218, - 0.36862745098039218, - 1.0), - 'grey38': ( 0.38039215686274508, - 0.38039215686274508, - 0.38039215686274508, - 1.0), - 'grey39': ( 0.38823529411764707, - 0.38823529411764707, - 0.38823529411764707, - 1.0), - 'grey4': ( 0.039215686274509803, - 0.039215686274509803, - 0.039215686274509803, - 1.0), - 'grey40': ( 0.40000000000000002, - 0.40000000000000002, - 0.40000000000000002, - 1.0), - 'grey41': ( 0.41176470588235292, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'grey42': ( 0.41960784313725491, - 0.41960784313725491, - 0.41960784313725491, - 1.0), - 'grey43': ( 0.43137254901960786, - 0.43137254901960786, - 0.43137254901960786, - 1.0), - 'grey44': ( 0.4392156862745098, - 0.4392156862745098, - 0.4392156862745098, - 1.0), - 'grey45': ( 0.45098039215686275, - 0.45098039215686275, - 0.45098039215686275, - 1.0), - 'grey46': ( 0.45882352941176469, - 0.45882352941176469, - 0.45882352941176469, - 1.0), - 'grey47': ( 0.47058823529411764, - 0.47058823529411764, - 0.47058823529411764, - 1.0), - 'grey48': ( 0.47843137254901963, - 0.47843137254901963, - 0.47843137254901963, - 1.0), - 'grey49': ( 0.49019607843137253, - 0.49019607843137253, - 0.49019607843137253, - 1.0), - 'grey5': ( 0.050980392156862744, - 0.050980392156862744, - 0.050980392156862744, - 1.0), - 'grey50': ( 0.49803921568627452, - 0.49803921568627452, - 0.49803921568627452, - 1.0), - 'grey51': ( 0.50980392156862742, - 0.50980392156862742, - 0.50980392156862742, - 1.0), - 'grey52': ( 0.52156862745098043, - 0.52156862745098043, - 0.52156862745098043, - 1.0), - 'grey53': ( 0.52941176470588236, - 0.52941176470588236, - 0.52941176470588236, - 1.0), - 'grey54': ( 0.54117647058823526, - 0.54117647058823526, - 0.54117647058823526, - 1.0), - 'grey55': ( 0.5490196078431373, - 0.5490196078431373, - 0.5490196078431373, - 1.0), - 'grey56': ( 0.5607843137254902, - 0.5607843137254902, - 0.5607843137254902, - 1.0), - 'grey57': ( 0.56862745098039214, - 0.56862745098039214, - 0.56862745098039214, - 1.0), - 'grey58': ( 0.58039215686274515, - 0.58039215686274515, - 0.58039215686274515, - 1.0), - 'grey59': ( 0.58823529411764708, - 0.58823529411764708, - 0.58823529411764708, - 1.0), - 'grey6': ( 0.058823529411764705, - 0.058823529411764705, - 0.058823529411764705, - 1.0), - 'grey60': ( 0.59999999999999998, - 0.59999999999999998, - 0.59999999999999998, - 1.0), - 'grey61': ( 0.61176470588235299, - 0.61176470588235299, - 0.61176470588235299, - 1.0), - 'grey62': ( 0.61960784313725492, - 0.61960784313725492, - 0.61960784313725492, - 1.0), - 'grey63': ( 0.63137254901960782, - 0.63137254901960782, - 0.63137254901960782, - 1.0), - 'grey64': ( 0.63921568627450975, - 0.63921568627450975, - 0.63921568627450975, - 1.0), - 'grey65': ( 0.65098039215686276, - 0.65098039215686276, - 0.65098039215686276, - 1.0), - 'grey66': ( 0.6588235294117647, - 0.6588235294117647, - 0.6588235294117647, - 1.0), - 'grey67': ( 0.6705882352941176, - 0.6705882352941176, - 0.6705882352941176, - 1.0), - 'grey68': ( 0.67843137254901964, - 0.67843137254901964, - 0.67843137254901964, - 1.0), - 'grey69': ( 0.69019607843137254, - 0.69019607843137254, - 0.69019607843137254, - 1.0), - 'grey7': ( 0.070588235294117646, - 0.070588235294117646, - 0.070588235294117646, - 1.0), - 'grey70': ( 0.70196078431372544, - 0.70196078431372544, - 0.70196078431372544, - 1.0), - 'grey71': ( 0.70980392156862748, - 0.70980392156862748, - 0.70980392156862748, - 1.0), - 'grey72': ( 0.72156862745098038, - 0.72156862745098038, - 0.72156862745098038, - 1.0), - 'grey73': ( 0.72941176470588232, - 0.72941176470588232, - 0.72941176470588232, - 1.0), - 'grey74': ( 0.74117647058823533, - 0.74117647058823533, - 0.74117647058823533, - 1.0), - 'grey75': ( 0.74901960784313726, - 0.74901960784313726, - 0.74901960784313726, - 1.0), - 'grey76': ( 0.76078431372549016, - 0.76078431372549016, - 0.76078431372549016, - 1.0), - 'grey77': ( 0.7686274509803922, - 0.7686274509803922, - 0.7686274509803922, - 1.0), - 'grey78': ( 0.7803921568627451, - 0.7803921568627451, - 0.7803921568627451, - 1.0), - 'grey79': ( 0.78823529411764703, - 0.78823529411764703, - 0.78823529411764703, - 1.0), - 'grey8': ( 0.078431372549019607, - 0.078431372549019607, - 0.078431372549019607, - 1.0), - 'grey80': ( 0.80000000000000004, - 0.80000000000000004, - 0.80000000000000004, - 1.0), - 'grey81': ( 0.81176470588235294, - 0.81176470588235294, - 0.81176470588235294, - 1.0), - 'grey82': ( 0.81960784313725488, - 0.81960784313725488, - 0.81960784313725488, - 1.0), - 'grey83': ( 0.83137254901960789, - 0.83137254901960789, - 0.83137254901960789, - 1.0), - 'grey84': ( 0.83921568627450982, - 0.83921568627450982, - 0.83921568627450982, - 1.0), - 'grey85': ( 0.85098039215686272, - 0.85098039215686272, - 0.85098039215686272, - 1.0), - 'grey86': ( 0.85882352941176465, - 0.85882352941176465, - 0.85882352941176465, - 1.0), - 'grey87': ( 0.87058823529411766, - 0.87058823529411766, - 0.87058823529411766, - 1.0), - 'grey88': ( 0.8784313725490196, - 0.8784313725490196, - 0.8784313725490196, - 1.0), - 'grey89': ( 0.8901960784313725, - 0.8901960784313725, - 0.8901960784313725, - 1.0), - 'grey9': ( 0.090196078431372548, - 0.090196078431372548, - 0.090196078431372548, - 1.0), - 'grey90': ( 0.89803921568627454, - 0.89803921568627454, - 0.89803921568627454, - 1.0), - 'grey91': ( 0.90980392156862744, - 0.90980392156862744, - 0.90980392156862744, - 1.0), - 'grey92': ( 0.92156862745098034, - 0.92156862745098034, - 0.92156862745098034, - 1.0), - 'grey93': ( 0.92941176470588238, - 0.92941176470588238, - 0.92941176470588238, - 1.0), - 'grey94': ( 0.94117647058823528, - 0.94117647058823528, - 0.94117647058823528, - 1.0), - 'grey95': ( 0.94901960784313721, - 0.94901960784313721, - 0.94901960784313721, - 1.0), - 'grey96': ( 0.96078431372549022, - 0.96078431372549022, - 0.96078431372549022, - 1.0), - 'grey97': ( 0.96862745098039216, - 0.96862745098039216, - 0.96862745098039216, - 1.0), - 'grey98': ( 0.98039215686274506, - 0.98039215686274506, - 0.98039215686274506, - 1.0), - 'grey99': ( 0.9882352941176471, - 0.9882352941176471, - 0.9882352941176471, - 1.0), - 'honeydew': (0.94117647058823528, 1.0, 0.94117647058823528, 1.0), - 'honeydew1': (0.94117647058823528, 1.0, 0.94117647058823528, 1.0), - 'honeydew2': ( 0.8784313725490196, - 0.93333333333333335, - 0.8784313725490196, - 1.0), - 'honeydew3': ( 0.75686274509803919, - 0.80392156862745101, - 0.75686274509803919, - 1.0), - 'honeydew4': ( 0.51372549019607838, - 0.54509803921568623, - 0.51372549019607838, - 1.0), - 'hot pink': (1.0, 0.41176470588235292, 0.70588235294117652, 1.0), - 'hotpink': (1.0, 0.41176470588235292, 0.70588235294117652, 1.0), - 'hotpink1': (1.0, 0.43137254901960786, 0.70588235294117652, 1.0), - 'hotpink2': ( 0.93333333333333335, - 0.41568627450980394, - 0.65490196078431373, - 1.0), - 'hotpink3': ( 0.80392156862745101, - 0.37647058823529411, - 0.56470588235294117, - 1.0), - 'hotpink4': ( 0.54509803921568623, - 0.22745098039215686, - 0.3843137254901961, - 1.0), - 'indian red': ( 0.80392156862745101, - 0.36078431372549019, - 0.36078431372549019, - 1.0), - 'indianred': ( 0.80392156862745101, - 0.36078431372549019, - 0.36078431372549019, - 1.0), - 'indianred1': (1.0, 0.41568627450980394, 0.41568627450980394, 1.0), - 'indianred2': ( 0.93333333333333335, - 0.38823529411764707, - 0.38823529411764707, - 1.0), - 'indianred3': ( 0.80392156862745101, - 0.33333333333333331, - 0.33333333333333331, - 1.0), - 'indianred4': ( 0.54509803921568623, - 0.22745098039215686, - 0.22745098039215686, - 1.0), - 'indigo': (0.29411764705882354, 0.0, 0.5098039215686274, 1.0), - 'ivory': (1.0, 1.0, 0.94117647058823528, 1.0), - 'ivory1': (1.0, 1.0, 0.94117647058823528, 1.0), - 'ivory2': ( 0.93333333333333335, - 0.93333333333333335, - 0.8784313725490196, - 1.0), - 'ivory3': ( 0.80392156862745101, - 0.80392156862745101, - 0.75686274509803919, - 1.0), - 'ivory4': ( 0.54509803921568623, - 0.54509803921568623, - 0.51372549019607838, - 1.0), - 'khaki': ( 0.94117647058823528, - 0.90196078431372551, - 0.5490196078431373, - 1.0), - 'khaki1': (1.0, 0.96470588235294119, 0.5607843137254902, 1.0), - 'khaki2': ( 0.93333333333333335, - 0.90196078431372551, - 0.52156862745098043, - 1.0), - 'khaki3': ( 0.80392156862745101, - 0.77647058823529413, - 0.45098039215686275, - 1.0), - 'khaki4': ( 0.54509803921568623, - 0.52549019607843139, - 0.30588235294117649, - 1.0), - 'lavender': ( 0.90196078431372551, - 0.90196078431372551, - 0.98039215686274506, - 1.0), - 'lavender blush': (1.0, 0.94117647058823528, 0.96078431372549022, 1.0), - 'lavenderblush': (1.0, 0.94117647058823528, 0.96078431372549022, 1.0), - 'lavenderblush1': (1.0, 0.94117647058823528, 0.96078431372549022, 1.0), - 'lavenderblush2': ( 0.93333333333333335, - 0.8784313725490196, - 0.89803921568627454, - 1.0), - 'lavenderblush3': ( 0.80392156862745101, - 0.75686274509803919, - 0.77254901960784317, - 1.0), - 'lavenderblush4': ( 0.54509803921568623, - 0.51372549019607838, - 0.52549019607843139, - 1.0), - 'lawn green': (0.48627450980392156, 0.9882352941176471, 0.0, 1.0), - 'lawngreen': (0.48627450980392156, 0.9882352941176471, 0.0, 1.0), - 'lemon chiffon': (1.0, 0.98039215686274506, 0.80392156862745101, 1.0), - 'lemonchiffon': (1.0, 0.98039215686274506, 0.80392156862745101, 1.0), - 'lemonchiffon1': (1.0, 0.98039215686274506, 0.80392156862745101, 1.0), - 'lemonchiffon2': ( 0.93333333333333335, - 0.9137254901960784, - 0.74901960784313726, - 1.0), - 'lemonchiffon3': ( 0.80392156862745101, - 0.78823529411764703, - 0.6470588235294118, - 1.0), - 'lemonchiffon4': ( 0.54509803921568623, - 0.53725490196078429, - 0.4392156862745098, - 1.0), - 'light blue': ( 0.67843137254901964, - 0.84705882352941175, - 0.90196078431372551, - 1.0), - 'light coral': ( 0.94117647058823528, - 0.50196078431372548, - 0.50196078431372548, - 1.0), - 'light cyan': (0.8784313725490196, 1.0, 1.0, 1.0), - 'light goldenrod': ( 0.93333333333333335, - 0.8666666666666667, - 0.50980392156862742, - 1.0), - 'light goldenrod yellow': ( 0.98039215686274506, - 0.98039215686274506, - 0.82352941176470584, - 1.0), - 'light gray': ( 0.82745098039215681, - 0.82745098039215681, - 0.82745098039215681, - 1.0), - 'light green': ( 0.56470588235294117, - 0.93333333333333335, - 0.56470588235294117, - 1.0), - 'light grey': ( 0.82745098039215681, - 0.82745098039215681, - 0.82745098039215681, - 1.0), - 'light pink': (1.0, 0.71372549019607845, 0.75686274509803919, 1.0), - 'light salmon': (1.0, 0.62745098039215685, 0.47843137254901963, 1.0), - 'light sea green': ( 0.12549019607843137, - 0.69803921568627447, - 0.66666666666666663, - 1.0), - 'light sky blue': ( 0.52941176470588236, - 0.80784313725490198, - 0.98039215686274506, - 1.0), - 'light slate blue': (0.51764705882352946, 0.4392156862745098, 1.0, 1.0), - 'light slate gray': ( 0.46666666666666667, - 0.53333333333333333, - 0.59999999999999998, - 1.0), - 'light slate grey': ( 0.46666666666666667, - 0.53333333333333333, - 0.59999999999999998, - 1.0), - 'light steel blue': ( 0.69019607843137254, - 0.7686274509803922, - 0.87058823529411766, - 1.0), - 'light yellow': (1.0, 1.0, 0.8784313725490196, 1.0), - 'lightblue': ( 0.67843137254901964, - 0.84705882352941175, - 0.90196078431372551, - 1.0), - 'lightblue1': (0.74901960784313726, 0.93725490196078431, 1.0, 1.0), - 'lightblue2': ( 0.69803921568627447, - 0.87450980392156863, - 0.93333333333333335, - 1.0), - 'lightblue3': ( 0.60392156862745094, - 0.75294117647058822, - 0.80392156862745101, - 1.0), - 'lightblue4': ( 0.40784313725490196, - 0.51372549019607838, - 0.54509803921568623, - 1.0), - 'lightcoral': ( 0.94117647058823528, - 0.50196078431372548, - 0.50196078431372548, - 1.0), - 'lightcyan': (0.8784313725490196, 1.0, 1.0, 1.0), - 'lightcyan1': (0.8784313725490196, 1.0, 1.0, 1.0), - 'lightcyan2': ( 0.81960784313725488, - 0.93333333333333335, - 0.93333333333333335, - 1.0), - 'lightcyan3': ( 0.70588235294117652, - 0.80392156862745101, - 0.80392156862745101, - 1.0), - 'lightcyan4': ( 0.47843137254901963, - 0.54509803921568623, - 0.54509803921568623, - 1.0), - 'lightgoldenrod': ( 0.93333333333333335, - 0.8666666666666667, - 0.50980392156862742, - 1.0), - 'lightgoldenrod1': (1.0, 0.92549019607843142, 0.54509803921568623, 1.0), - 'lightgoldenrod2': ( 0.93333333333333335, - 0.86274509803921573, - 0.50980392156862742, - 1.0), - 'lightgoldenrod3': ( 0.80392156862745101, - 0.74509803921568629, - 0.4392156862745098, - 1.0), - 'lightgoldenrod4': ( 0.54509803921568623, - 0.50588235294117645, - 0.29803921568627451, - 1.0), - 'lightgoldenrodyellow': ( 0.98039215686274506, - 0.98039215686274506, - 0.82352941176470584, - 1.0), - 'lightgray': ( 0.82745098039215681, - 0.82745098039215681, - 0.82745098039215681, - 1.0), - 'lightgreen': ( 0.56470588235294117, - 0.93333333333333335, - 0.56470588235294117, - 1.0), - 'lightgrey': ( 0.82745098039215681, - 0.82745098039215681, - 0.82745098039215681, - 1.0), - 'lightpink': (1.0, 0.71372549019607845, 0.75686274509803919, 1.0), - 'lightpink1': (1.0, 0.68235294117647061, 0.72549019607843135, 1.0), - 'lightpink2': ( 0.93333333333333335, - 0.63529411764705879, - 0.67843137254901964, - 1.0), - 'lightpink3': ( 0.80392156862745101, - 0.5490196078431373, - 0.58431372549019611, - 1.0), - 'lightpink4': ( 0.54509803921568623, - 0.37254901960784315, - 0.396078431372549, - 1.0), - 'lightsalmon': (1.0, 0.62745098039215685, 0.47843137254901963, 1.0), - 'lightsalmon1': (1.0, 0.62745098039215685, 0.47843137254901963, 1.0), - 'lightsalmon2': ( 0.93333333333333335, - 0.58431372549019611, - 0.44705882352941179, - 1.0), - 'lightsalmon3': ( 0.80392156862745101, - 0.50588235294117645, - 0.3843137254901961, - 1.0), - 'lightsalmon4': ( 0.54509803921568623, - 0.3411764705882353, - 0.25882352941176473, - 1.0), - 'lightseagreen': ( 0.12549019607843137, - 0.69803921568627447, - 0.66666666666666663, - 1.0), - 'lightskyblue': ( 0.52941176470588236, - 0.80784313725490198, - 0.98039215686274506, - 1.0), - 'lightskyblue1': (0.69019607843137254, 0.88627450980392153, 1.0, 1.0), - 'lightskyblue2': ( 0.64313725490196083, - 0.82745098039215681, - 0.93333333333333335, - 1.0), - 'lightskyblue3': ( 0.55294117647058827, - 0.71372549019607845, - 0.80392156862745101, - 1.0), - 'lightskyblue4': ( 0.37647058823529411, - 0.4823529411764706, - 0.54509803921568623, - 1.0), - 'lightslateblue': (0.51764705882352946, 0.4392156862745098, 1.0, 1.0), - 'lightslategray': ( 0.46666666666666667, - 0.53333333333333333, - 0.59999999999999998, - 1.0), - 'lightslategrey': ( 0.46666666666666667, - 0.53333333333333333, - 0.59999999999999998, - 1.0), - 'lightsteelblue': ( 0.69019607843137254, - 0.7686274509803922, - 0.87058823529411766, - 1.0), - 'lightsteelblue1': (0.792156862745098, 0.88235294117647056, 1.0, 1.0), - 'lightsteelblue2': ( 0.73725490196078436, - 0.82352941176470584, - 0.93333333333333335, - 1.0), - 'lightsteelblue3': ( 0.63529411764705879, - 0.70980392156862748, - 0.80392156862745101, - 1.0), - 'lightsteelblue4': ( 0.43137254901960786, - 0.4823529411764706, - 0.54509803921568623, - 1.0), - 'lightyellow': (1.0, 1.0, 0.8784313725490196, 1.0), - 'lightyellow1': (1.0, 1.0, 0.8784313725490196, 1.0), - 'lightyellow2': ( 0.93333333333333335, - 0.93333333333333335, - 0.81960784313725488, - 1.0), - 'lightyellow3': ( 0.80392156862745101, - 0.80392156862745101, - 0.70588235294117652, - 1.0), - 'lightyellow4': ( 0.54509803921568623, - 0.54509803921568623, - 0.47843137254901963, - 1.0), - 'lime': (0.0, 1.0, 0.0, 1.0), - 'lime green': ( 0.19607843137254902, - 0.80392156862745101, - 0.19607843137254902, - 1.0), - 'limegreen': ( 0.19607843137254902, - 0.80392156862745101, - 0.19607843137254902, - 1.0), - 'linen': ( 0.98039215686274506, - 0.94117647058823528, - 0.90196078431372551, - 1.0), - 'magenta': (1.0, 0.0, 1.0, 1.0), - 'magenta1': (1.0, 0.0, 1.0, 1.0), - 'magenta2': (0.93333333333333335, 0.0, 0.93333333333333335, 1.0), - 'magenta3': (0.80392156862745101, 0.0, 0.80392156862745101, 1.0), - 'magenta4': (0.54509803921568623, 0.0, 0.54509803921568623, 1.0), - 'maroon': ( 0.69019607843137254, - 0.18823529411764706, - 0.37647058823529411, - 1.0), - 'maroon1': (1.0, 0.20392156862745098, 0.70196078431372544, 1.0), - 'maroon2': ( 0.93333333333333335, - 0.18823529411764706, - 0.65490196078431373, - 1.0), - 'maroon3': ( 0.80392156862745101, - 0.16078431372549021, - 0.56470588235294117, - 1.0), - 'maroon4': ( 0.54509803921568623, - 0.10980392156862745, - 0.3843137254901961, - 1.0), - 'medium aquamarine': ( 0.40000000000000002, - 0.80392156862745101, - 0.66666666666666663, - 1.0), - 'medium blue': (0.0, 0.0, 0.80392156862745101, 1.0), - 'medium orchid': ( 0.72941176470588232, - 0.33333333333333331, - 0.82745098039215681, - 1.0), - 'medium purple': ( 0.57647058823529407, - 0.4392156862745098, - 0.85882352941176465, - 1.0), - 'medium sea green': ( 0.23529411764705882, - 0.70196078431372544, - 0.44313725490196076, - 1.0), - 'medium slate blue': ( 0.4823529411764706, - 0.40784313725490196, - 0.93333333333333335, - 1.0), - 'medium spring green': ( 0.0, - 0.98039215686274506, - 0.60392156862745094, - 1.0), - 'medium turquoise': ( 0.28235294117647058, - 0.81960784313725488, - 0.80000000000000004, - 1.0), - 'medium violet red': ( 0.7803921568627451, - 0.082352941176470587, - 0.52156862745098043, - 1.0), - 'mediumaquamarine': ( 0.40000000000000002, - 0.80392156862745101, - 0.66666666666666663, - 1.0), - 'mediumblue': (0.0, 0.0, 0.80392156862745101, 1.0), - 'mediumorchid': ( 0.72941176470588232, - 0.33333333333333331, - 0.82745098039215681, - 1.0), - 'mediumorchid1': (0.8784313725490196, 0.40000000000000002, 1.0, 1.0), - 'mediumorchid2': ( 0.81960784313725488, - 0.37254901960784315, - 0.93333333333333335, - 1.0), - 'mediumorchid3': ( 0.70588235294117652, - 0.32156862745098042, - 0.80392156862745101, - 1.0), - 'mediumorchid4': ( 0.47843137254901963, - 0.21568627450980393, - 0.54509803921568623, - 1.0), - 'mediumpurple': ( 0.57647058823529407, - 0.4392156862745098, - 0.85882352941176465, - 1.0), - 'mediumpurple1': (0.6705882352941176, 0.50980392156862742, 1.0, 1.0), - 'mediumpurple2': ( 0.62352941176470589, - 0.47450980392156861, - 0.93333333333333335, - 1.0), - 'mediumpurple3': ( 0.53725490196078429, - 0.40784313725490196, - 0.80392156862745101, - 1.0), - 'mediumpurple4': ( 0.36470588235294116, - 0.27843137254901962, - 0.54509803921568623, - 1.0), - 'mediumseagreen': ( 0.23529411764705882, - 0.70196078431372544, - 0.44313725490196076, - 1.0), - 'mediumslateblue': ( 0.4823529411764706, - 0.40784313725490196, - 0.93333333333333335, - 1.0), - 'mediumspringgreen': (0.0, 0.98039215686274506, 0.60392156862745094, 1.0), - 'mediumturquoise': ( 0.28235294117647058, - 0.81960784313725488, - 0.80000000000000004, - 1.0), - 'mediumvioletred': ( 0.7803921568627451, - 0.082352941176470587, - 0.52156862745098043, - 1.0), - 'midnight blue': ( 0.098039215686274508, - 0.098039215686274508, - 0.4392156862745098, - 1.0), - 'midnightblue': ( 0.098039215686274508, - 0.098039215686274508, - 0.4392156862745098, - 1.0), - 'mint cream': (0.96078431372549022, 1.0, 0.98039215686274506, 1.0), - 'mintcream': (0.96078431372549022, 1.0, 0.98039215686274506, 1.0), - 'misty rose': (1.0, 0.89411764705882357, 0.88235294117647056, 1.0), - 'mistyrose': (1.0, 0.89411764705882357, 0.88235294117647056, 1.0), - 'mistyrose1': (1.0, 0.89411764705882357, 0.88235294117647056, 1.0), - 'mistyrose2': ( 0.93333333333333335, - 0.83529411764705885, - 0.82352941176470584, - 1.0), - 'mistyrose3': ( 0.80392156862745101, - 0.71764705882352942, - 0.70980392156862748, - 1.0), - 'mistyrose4': ( 0.54509803921568623, - 0.49019607843137253, - 0.4823529411764706, - 1.0), - 'moccasin': (1.0, 0.89411764705882357, 0.70980392156862748, 1.0), - 'navajo white': (1.0, 0.87058823529411766, 0.67843137254901964, 1.0), - 'navajowhite': (1.0, 0.87058823529411766, 0.67843137254901964, 1.0), - 'navajowhite1': (1.0, 0.87058823529411766, 0.67843137254901964, 1.0), - 'navajowhite2': ( 0.93333333333333335, - 0.81176470588235294, - 0.63137254901960782, - 1.0), - 'navajowhite3': ( 0.80392156862745101, - 0.70196078431372544, - 0.54509803921568623, - 1.0), - 'navajowhite4': ( 0.54509803921568623, - 0.47450980392156861, - 0.36862745098039218, - 1.0), - 'navy': (0.0, 0.0, 0.50196078431372548, 1.0), - 'navy blue': (0.0, 0.0, 0.50196078431372548, 1.0), - 'navyblue': (0.0, 0.0, 0.50196078431372548, 1.0), - 'old lace': ( 0.99215686274509807, - 0.96078431372549022, - 0.90196078431372551, - 1.0), - 'oldlace': ( 0.99215686274509807, - 0.96078431372549022, - 0.90196078431372551, - 1.0), - 'olive': (0.5, 0.5, 0.0, 1.0), - 'olive drab': ( 0.41960784313725491, - 0.55686274509803924, - 0.13725490196078433, - 1.0), - 'olivedrab': ( 0.41960784313725491, - 0.55686274509803924, - 0.13725490196078433, - 1.0), - 'olivedrab1': (0.75294117647058822, 1.0, 0.24313725490196078, 1.0), - 'olivedrab2': ( 0.70196078431372544, - 0.93333333333333335, - 0.22745098039215686, - 1.0), - 'olivedrab3': ( 0.60392156862745094, - 0.80392156862745101, - 0.19607843137254902, - 1.0), - 'olivedrab4': ( 0.41176470588235292, - 0.54509803921568623, - 0.13333333333333333, - 1.0), - 'orange': (1.0, 0.6470588235294118, 0.0, 1.0), - 'orange red': (1.0, 0.27058823529411763, 0.0, 1.0), - 'orange1': (1.0, 0.6470588235294118, 0.0, 1.0), - 'orange2': (0.93333333333333335, 0.60392156862745094, 0.0, 1.0), - 'orange3': (0.80392156862745101, 0.52156862745098043, 0.0, 1.0), - 'orange4': (0.54509803921568623, 0.35294117647058826, 0.0, 1.0), - 'orangered': (1.0, 0.27058823529411763, 0.0, 1.0), - 'orangered1': (1.0, 0.27058823529411763, 0.0, 1.0), - 'orangered2': (0.93333333333333335, 0.25098039215686274, 0.0, 1.0), - 'orangered3': (0.80392156862745101, 0.21568627450980393, 0.0, 1.0), - 'orangered4': (0.54509803921568623, 0.14509803921568629, 0.0, 1.0), - 'orchid': ( 0.85490196078431369, - 0.4392156862745098, - 0.83921568627450982, - 1.0), - 'orchid1': (1.0, 0.51372549019607838, 0.98039215686274506, 1.0), - 'orchid2': ( 0.93333333333333335, - 0.47843137254901963, - 0.9137254901960784, - 1.0), - 'orchid3': ( 0.80392156862745101, - 0.41176470588235292, - 0.78823529411764703, - 1.0), - 'orchid4': ( 0.54509803921568623, - 0.27843137254901962, - 0.53725490196078429, - 1.0), - 'pale goldenrod': ( 0.93333333333333335, - 0.90980392156862744, - 0.66666666666666663, - 1.0), - 'pale green': ( 0.59607843137254901, - 0.98431372549019602, - 0.59607843137254901, - 1.0), - 'pale turquoise': ( 0.68627450980392157, - 0.93333333333333335, - 0.93333333333333335, - 1.0), - 'pale violet red': ( 0.85882352941176465, - 0.4392156862745098, - 0.57647058823529407, - 1.0), - 'palegoldenrod': ( 0.93333333333333335, - 0.90980392156862744, - 0.66666666666666663, - 1.0), - 'palegreen': ( 0.59607843137254901, - 0.98431372549019602, - 0.59607843137254901, - 1.0), - 'palegreen1': (0.60392156862745094, 1.0, 0.60392156862745094, 1.0), - 'palegreen2': ( 0.56470588235294117, - 0.93333333333333335, - 0.56470588235294117, - 1.0), - 'palegreen3': ( 0.48627450980392156, - 0.80392156862745101, - 0.48627450980392156, - 1.0), - 'palegreen4': ( 0.32941176470588235, - 0.54509803921568623, - 0.32941176470588235, - 1.0), - 'paleturquoise': ( 0.68627450980392157, - 0.93333333333333335, - 0.93333333333333335, - 1.0), - 'paleturquoise1': (0.73333333333333328, 1.0, 1.0, 1.0), - 'paleturquoise2': ( 0.68235294117647061, - 0.93333333333333335, - 0.93333333333333335, - 1.0), - 'paleturquoise3': ( 0.58823529411764708, - 0.80392156862745101, - 0.80392156862745101, - 1.0), - 'paleturquoise4': ( 0.40000000000000002, - 0.54509803921568623, - 0.54509803921568623, - 1.0), - 'palevioletred': ( 0.85882352941176465, - 0.4392156862745098, - 0.57647058823529407, - 1.0), - 'palevioletred1': (1.0, 0.50980392156862742, 0.6705882352941176, 1.0), - 'palevioletred2': ( 0.93333333333333335, - 0.47450980392156861, - 0.62352941176470589, - 1.0), - 'palevioletred3': ( 0.80392156862745101, - 0.40784313725490196, - 0.53725490196078429, - 1.0), - 'palevioletred4': ( 0.54509803921568623, - 0.27843137254901962, - 0.36470588235294116, - 1.0), - 'papaya whip': (1.0, 0.93725490196078431, 0.83529411764705885, 1.0), - 'papayawhip': (1.0, 0.93725490196078431, 0.83529411764705885, 1.0), - 'peach puff': (1.0, 0.85490196078431369, 0.72549019607843135, 1.0), - 'peachpuff': (1.0, 0.85490196078431369, 0.72549019607843135, 1.0), - 'peachpuff1': (1.0, 0.85490196078431369, 0.72549019607843135, 1.0), - 'peachpuff2': ( 0.93333333333333335, - 0.79607843137254897, - 0.67843137254901964, - 1.0), - 'peachpuff3': ( 0.80392156862745101, - 0.68627450980392157, - 0.58431372549019611, - 1.0), - 'peachpuff4': ( 0.54509803921568623, - 0.46666666666666667, - 0.396078431372549, - 1.0), - 'peru': ( 0.80392156862745101, - 0.52156862745098043, - 0.24705882352941178, - 1.0), - 'pink': (1.0, 0.75294117647058822, 0.79607843137254897, 1.0), - 'pink1': (1.0, 0.70980392156862748, 0.77254901960784317, 1.0), - 'pink2': ( 0.93333333333333335, - 0.66274509803921566, - 0.72156862745098038, - 1.0), - 'pink3': ( 0.80392156862745101, - 0.56862745098039214, - 0.61960784313725492, - 1.0), - 'pink4': ( 0.54509803921568623, - 0.38823529411764707, - 0.42352941176470588, - 1.0), - 'plum': (0.8666666666666667, 0.62745098039215685, 0.8666666666666667, 1.0), - 'plum1': (1.0, 0.73333333333333328, 1.0, 1.0), - 'plum2': ( 0.93333333333333335, - 0.68235294117647061, - 0.93333333333333335, - 1.0), - 'plum3': ( 0.80392156862745101, - 0.58823529411764708, - 0.80392156862745101, - 1.0), - 'plum4': ( 0.54509803921568623, - 0.40000000000000002, - 0.54509803921568623, - 1.0), - 'powder blue': ( 0.69019607843137254, - 0.8784313725490196, - 0.90196078431372551, - 1.0), - 'powderblue': ( 0.69019607843137254, - 0.8784313725490196, - 0.90196078431372551, - 1.0), - 'purple': ( 0.62745098039215685, - 0.12549019607843137, - 0.94117647058823528, - 1.0), - 'purple1': (0.60784313725490191, 0.18823529411764706, 1.0, 1.0), - 'purple2': ( 0.56862745098039214, - 0.17254901960784313, - 0.93333333333333335, - 1.0), - 'purple3': ( 0.49019607843137253, - 0.14901960784313725, - 0.80392156862745101, - 1.0), - 'purple4': ( 0.33333333333333331, - 0.10196078431372549, - 0.54509803921568623, - 1.0), - 'rebecca purple': (0.4, 0.2, 0.6, 1.0), - 'rebeccapurple': (0.4, 0.2, 0.6, 1.0), - 'red': (1.0, 0.0, 0.0, 1.0), - 'red1': (1.0, 0.0, 0.0, 1.0), - 'red2': (0.93333333333333335, 0.0, 0.0, 1.0), - 'red3': (0.80392156862745101, 0.0, 0.0, 1.0), - 'red4': (0.54509803921568623, 0.0, 0.0, 1.0), - 'rosy brown': ( 0.73725490196078436, - 0.5607843137254902, - 0.5607843137254902, - 1.0), - 'rosybrown': ( 0.73725490196078436, - 0.5607843137254902, - 0.5607843137254902, - 1.0), - 'rosybrown1': (1.0, 0.75686274509803919, 0.75686274509803919, 1.0), - 'rosybrown2': ( 0.93333333333333335, - 0.70588235294117652, - 0.70588235294117652, - 1.0), - 'rosybrown3': ( 0.80392156862745101, - 0.60784313725490191, - 0.60784313725490191, - 1.0), - 'rosybrown4': ( 0.54509803921568623, - 0.41176470588235292, - 0.41176470588235292, - 1.0), - 'royal blue': ( 0.25490196078431371, - 0.41176470588235292, - 0.88235294117647056, - 1.0), - 'royalblue': ( 0.25490196078431371, - 0.41176470588235292, - 0.88235294117647056, - 1.0), - 'royalblue1': (0.28235294117647058, 0.46274509803921571, 1.0, 1.0), - 'royalblue2': ( 0.2627450980392157, - 0.43137254901960786, - 0.93333333333333335, - 1.0), - 'royalblue3': ( 0.22745098039215686, - 0.37254901960784315, - 0.80392156862745101, - 1.0), - 'royalblue4': ( 0.15294117647058825, - 0.25098039215686274, - 0.54509803921568623, - 1.0), - 'saddle brown': ( 0.54509803921568623, - 0.27058823529411763, - 0.074509803921568626, - 1.0), - 'saddlebrown': ( 0.54509803921568623, - 0.27058823529411763, - 0.074509803921568626, - 1.0), - 'salmon': ( 0.98039215686274506, - 0.50196078431372548, - 0.44705882352941179, - 1.0), - 'salmon1': (1.0, 0.5490196078431373, 0.41176470588235292, 1.0), - 'salmon2': ( 0.93333333333333335, - 0.50980392156862742, - 0.3843137254901961, - 1.0), - 'salmon3': ( 0.80392156862745101, - 0.4392156862745098, - 0.32941176470588235, - 1.0), - 'salmon4': ( 0.54509803921568623, - 0.29803921568627451, - 0.22352941176470589, - 1.0), - 'sandy brown': ( 0.95686274509803926, - 0.64313725490196083, - 0.37647058823529411, - 1.0), - 'sandybrown': ( 0.95686274509803926, - 0.64313725490196083, - 0.37647058823529411, - 1.0), - 'sea green': ( 0.1803921568627451, - 0.54509803921568623, - 0.3411764705882353, - 1.0), - 'seagreen': ( 0.1803921568627451, - 0.54509803921568623, - 0.3411764705882353, - 1.0), - 'seagreen1': (0.32941176470588235, 1.0, 0.62352941176470589, 1.0), - 'seagreen2': ( 0.30588235294117649, - 0.93333333333333335, - 0.58039215686274515, - 1.0), - 'seagreen3': ( 0.2627450980392157, - 0.80392156862745101, - 0.50196078431372548, - 1.0), - 'seagreen4': ( 0.1803921568627451, - 0.54509803921568623, - 0.3411764705882353, - 1.0), - 'seashell': (1.0, 0.96078431372549022, 0.93333333333333335, 1.0), - 'seashell1': (1.0, 0.96078431372549022, 0.93333333333333335, 1.0), - 'seashell2': ( 0.93333333333333335, - 0.89803921568627454, - 0.87058823529411766, - 1.0), - 'seashell3': ( 0.80392156862745101, - 0.77254901960784317, - 0.74901960784313726, - 1.0), - 'seashell4': ( 0.54509803921568623, - 0.52549019607843139, - 0.50980392156862742, - 1.0), - 'sienna': ( 0.62745098039215685, - 0.32156862745098042, - 0.17647058823529413, - 1.0), - 'sienna1': (1.0, 0.50980392156862742, 0.27843137254901962, 1.0), - 'sienna2': ( 0.93333333333333335, - 0.47450980392156861, - 0.25882352941176473, - 1.0), - 'sienna3': ( 0.80392156862745101, - 0.40784313725490196, - 0.22352941176470589, - 1.0), - 'sienna4': ( 0.54509803921568623, - 0.27843137254901962, - 0.14901960784313725, - 1.0), - 'silver': (0.75, 0.75, 0.75, 1.0), - 'sky blue': ( 0.52941176470588236, - 0.80784313725490198, - 0.92156862745098034, - 1.0), - 'skyblue': ( 0.52941176470588236, - 0.80784313725490198, - 0.92156862745098034, - 1.0), - 'skyblue1': (0.52941176470588236, 0.80784313725490198, 1.0, 1.0), - 'skyblue2': ( 0.49411764705882355, - 0.75294117647058822, - 0.93333333333333335, - 1.0), - 'skyblue3': ( 0.42352941176470588, - 0.65098039215686276, - 0.80392156862745101, - 1.0), - 'skyblue4': ( 0.29019607843137257, - 0.4392156862745098, - 0.54509803921568623, - 1.0), - 'slate blue': ( 0.41568627450980394, - 0.35294117647058826, - 0.80392156862745101, - 1.0), - 'slate gray': ( 0.4392156862745098, - 0.50196078431372548, - 0.56470588235294117, - 1.0), - 'slate grey': ( 0.4392156862745098, - 0.50196078431372548, - 0.56470588235294117, - 1.0), - 'slateblue': ( 0.41568627450980394, - 0.35294117647058826, - 0.80392156862745101, - 1.0), - 'slateblue1': (0.51372549019607838, 0.43529411764705883, 1.0, 1.0), - 'slateblue2': ( 0.47843137254901963, - 0.40392156862745099, - 0.93333333333333335, - 1.0), - 'slateblue3': ( 0.41176470588235292, - 0.34901960784313724, - 0.80392156862745101, - 1.0), - 'slateblue4': ( 0.27843137254901962, - 0.23529411764705882, - 0.54509803921568623, - 1.0), - 'slategray': ( 0.4392156862745098, - 0.50196078431372548, - 0.56470588235294117, - 1.0), - 'slategray1': (0.77647058823529413, 0.88627450980392153, 1.0, 1.0), - 'slategray2': ( 0.72549019607843135, - 0.82745098039215681, - 0.93333333333333335, - 1.0), - 'slategray3': ( 0.62352941176470589, - 0.71372549019607845, - 0.80392156862745101, - 1.0), - 'slategray4': ( 0.42352941176470588, - 0.4823529411764706, - 0.54509803921568623, - 1.0), - 'slategrey': ( 0.4392156862745098, - 0.50196078431372548, - 0.56470588235294117, - 1.0), - 'snow': (1.0, 0.98039215686274506, 0.98039215686274506, 1.0), - 'snow1': (1.0, 0.98039215686274506, 0.98039215686274506, 1.0), - 'snow2': ( 0.93333333333333335, - 0.9137254901960784, - 0.9137254901960784, - 1.0), - 'snow3': ( 0.80392156862745101, - 0.78823529411764703, - 0.78823529411764703, - 1.0), - 'snow4': ( 0.54509803921568623, - 0.53725490196078429, - 0.53725490196078429, - 1.0), - 'spring green': (0.0, 1.0, 0.49803921568627452, 1.0), - 'springgreen': (0.0, 1.0, 0.49803921568627452, 1.0), - 'springgreen1': (0.0, 1.0, 0.49803921568627452, 1.0), - 'springgreen2': (0.0, 0.93333333333333335, 0.46274509803921571, 1.0), - 'springgreen3': (0.0, 0.80392156862745101, 0.40000000000000002, 1.0), - 'springgreen4': (0.0, 0.54509803921568623, 0.27058823529411763, 1.0), - 'steel blue': ( 0.27450980392156865, - 0.50980392156862742, - 0.70588235294117652, - 1.0), - 'steelblue': ( 0.27450980392156865, - 0.50980392156862742, - 0.70588235294117652, - 1.0), - 'steelblue1': (0.38823529411764707, 0.72156862745098038, 1.0, 1.0), - 'steelblue2': ( 0.36078431372549019, - 0.67450980392156867, - 0.93333333333333335, - 1.0), - 'steelblue3': ( 0.30980392156862746, - 0.58039215686274515, - 0.80392156862745101, - 1.0), - 'steelblue4': ( 0.21176470588235294, - 0.39215686274509803, - 0.54509803921568623, - 1.0), - 'tan': (0.82352941176470584, 0.70588235294117652, 0.5490196078431373, 1.0), - 'tan1': (1.0, 0.6470588235294118, 0.30980392156862746, 1.0), - 'tan2': ( 0.93333333333333335, - 0.60392156862745094, - 0.28627450980392155, - 1.0), - 'tan3': ( 0.80392156862745101, - 0.52156862745098043, - 0.24705882352941178, - 1.0), - 'tan4': ( 0.54509803921568623, - 0.35294117647058826, - 0.16862745098039217, - 1.0), - 'teal': (0.0, 0.5, 0.5, 1.0), - 'thistle': ( 0.84705882352941175, - 0.74901960784313726, - 0.84705882352941175, - 1.0), - 'thistle1': (1.0, 0.88235294117647056, 1.0, 1.0), - 'thistle2': ( 0.93333333333333335, - 0.82352941176470584, - 0.93333333333333335, - 1.0), - 'thistle3': ( 0.80392156862745101, - 0.70980392156862748, - 0.80392156862745101, - 1.0), - 'thistle4': ( 0.54509803921568623, - 0.4823529411764706, - 0.54509803921568623, - 1.0), - 'tomato': (1.0, 0.38823529411764707, 0.27843137254901962, 1.0), - 'tomato1': (1.0, 0.38823529411764707, 0.27843137254901962, 1.0), - 'tomato2': ( 0.93333333333333335, - 0.36078431372549019, - 0.25882352941176473, - 1.0), - 'tomato3': ( 0.80392156862745101, - 0.30980392156862746, - 0.22352941176470589, - 1.0), - 'tomato4': ( 0.54509803921568623, - 0.21176470588235294, - 0.14901960784313725, - 1.0), - 'turquoise': ( 0.25098039215686274, - 0.8784313725490196, - 0.81568627450980391, - 1.0), - 'turquoise1': (0.0, 0.96078431372549022, 1.0, 1.0), - 'turquoise2': (0.0, 0.89803921568627454, 0.93333333333333335, 1.0), - 'turquoise3': (0.0, 0.77254901960784317, 0.80392156862745101, 1.0), - 'turquoise4': (0.0, 0.52549019607843139, 0.54509803921568623, 1.0), - 'violet': ( 0.93333333333333335, - 0.50980392156862742, - 0.93333333333333335, - 1.0), - 'violet red': ( 0.81568627450980391, - 0.12549019607843137, - 0.56470588235294117, - 1.0), - 'violetred': ( 0.81568627450980391, - 0.12549019607843137, - 0.56470588235294117, - 1.0), - 'violetred1': (1.0, 0.24313725490196078, 0.58823529411764708, 1.0), - 'violetred2': ( 0.93333333333333335, - 0.22745098039215686, - 0.5490196078431373, - 1.0), - 'violetred3': ( 0.80392156862745101, - 0.19607843137254902, - 0.47058823529411764, - 1.0), - 'violetred4': ( 0.54509803921568623, - 0.13333333333333333, - 0.32156862745098042, - 1.0), - 'web gray': ( 0.5019607843137255, - 0.5019607843137255, - 0.5019607843137255, - 1.0), - 'webgray': ( 0.5019607843137255, - 0.5019607843137255, - 0.5019607843137255, - 1.0), - 'web green': (0.0, 0.5019607843137255, 0.0, 1.0), - 'webgreen': (0.0, 0.5019607843137255, 0.0, 1.0), - 'webgray': ( 0.5019607843137255, - 0.5019607843137255, - 0.5019607843137255, - 1.0), - 'web grey': ( 0.5019607843137255, - 0.5019607843137255, - 0.5019607843137255, - 1.0), - 'webgrey': ( 0.5019607843137255, - 0.5019607843137255, - 0.5019607843137255, - 1.0), - 'web maroon': (0.5019607843137255, 0.0, 0.0, 1.0), - 'webmaroon': (0.5019607843137255, 0.0, 0.0, 1.0), - 'web purple': ( 0.4980392156862745, - 0.0, - 0.4980392156862745, - 1.0), - 'webpurple': ( 0.4980392156862745, - 0.0, - 0.4980392156862745, - 1.0), - 'wheat': ( 0.96078431372549022, - 0.87058823529411766, - 0.70196078431372544, - 1.0), - 'wheat1': (1.0, 0.90588235294117647, 0.72941176470588232, 1.0), - 'wheat2': ( 0.93333333333333335, - 0.84705882352941175, - 0.68235294117647061, - 1.0), - 'wheat3': ( 0.80392156862745101, - 0.72941176470588232, - 0.58823529411764708, - 1.0), - 'wheat4': ( 0.54509803921568623, - 0.49411764705882355, - 0.40000000000000002, - 1.0), - 'white': (1.0, 1.0, 1.0, 1.0), - 'white smoke': ( 0.96078431372549022, - 0.96078431372549022, - 0.96078431372549022, - 1.0), - 'whitesmoke': ( 0.96078431372549022, - 0.96078431372549022, - 0.96078431372549022, - 1.0), - 'yellow': (1.0, 1.0, 0.0, 1.0), - 'yellow green': ( 0.60392156862745094, - 0.80392156862745101, - 0.19607843137254902, - 1.0), - 'yellow1': (1.0, 1.0, 0.0, 1.0), - 'yellow2': (0.93333333333333335, 0.93333333333333335, 0.0, 1.0), - 'yellow3': (0.80392156862745101, 0.80392156862745101, 0.0, 1.0), - 'yellow4': (0.54509803921568623, 0.54509803921568623, 0.0, 1.0), - 'yellowgreen': ( 0.60392156862745094, - 0.80392156862745101, - 0.19607843137254902, - 1.0)} + return ( + red + (1.0 - red) * ratio, + green + (1.0 - green) * ratio, + blue + (1.0 - blue) * ratio, + alpha, + ) + + +known_colors = { + "alice blue": (0.94117647058823528, 0.97254901960784312, 1.0, 1.0), + "aliceblue": (0.94117647058823528, 0.97254901960784312, 1.0, 1.0), + "antique white": ( + 0.98039215686274506, + 0.92156862745098034, + 0.84313725490196079, + 1.0, + ), + "antiquewhite": ( + 0.98039215686274506, + 0.92156862745098034, + 0.84313725490196079, + 1.0, + ), + "antiquewhite1": (1.0, 0.93725490196078431, 0.85882352941176465, 1.0), + "antiquewhite2": ( + 0.93333333333333335, + 0.87450980392156863, + 0.80000000000000004, + 1.0, + ), + "antiquewhite3": ( + 0.80392156862745101, + 0.75294117647058822, + 0.69019607843137254, + 1.0, + ), + "antiquewhite4": ( + 0.54509803921568623, + 0.51372549019607838, + 0.47058823529411764, + 1.0, + ), + "aqua": (0.0, 1.0, 1.0, 1.0), + "aquamarine": (0.49803921568627452, 1.0, 0.83137254901960789, 1.0), + "aquamarine1": (0.49803921568627452, 1.0, 0.83137254901960789, 1.0), + "aquamarine2": (0.46274509803921571, 0.93333333333333335, 0.77647058823529413, 1.0), + "aquamarine3": (0.40000000000000002, 0.80392156862745101, 0.66666666666666663, 1.0), + "aquamarine4": (0.27058823529411763, 0.54509803921568623, 0.45490196078431372, 1.0), + "azure": (0.94117647058823528, 1.0, 1.0, 1.0), + "azure1": (0.94117647058823528, 1.0, 1.0, 1.0), + "azure2": (0.8784313725490196, 0.93333333333333335, 0.93333333333333335, 1.0), + "azure3": (0.75686274509803919, 0.80392156862745101, 0.80392156862745101, 1.0), + "azure4": (0.51372549019607838, 0.54509803921568623, 0.54509803921568623, 1.0), + "beige": (0.96078431372549022, 0.96078431372549022, 0.86274509803921573, 1.0), + "bisque": (1.0, 0.89411764705882357, 0.7686274509803922, 1.0), + "bisque1": (1.0, 0.89411764705882357, 0.7686274509803922, 1.0), + "bisque2": (0.93333333333333335, 0.83529411764705885, 0.71764705882352942, 1.0), + "bisque3": (0.80392156862745101, 0.71764705882352942, 0.61960784313725492, 1.0), + "bisque4": (0.54509803921568623, 0.49019607843137253, 0.41960784313725491, 1.0), + "black": (0.0, 0.0, 0.0, 1.0), + "blanched almond": (1.0, 0.92156862745098034, 0.80392156862745101, 1.0), + "blanchedalmond": (1.0, 0.92156862745098034, 0.80392156862745101, 1.0), + "blue": (0.0, 0.0, 1.0, 1.0), + "blue violet": (0.54117647058823526, 0.16862745098039217, 0.88627450980392153, 1.0), + "blue1": (0.0, 0.0, 1.0, 1.0), + "blue2": (0.0, 0.0, 0.93333333333333335, 1.0), + "blue3": (0.0, 0.0, 0.80392156862745101, 1.0), + "blue4": (0.0, 0.0, 0.54509803921568623, 1.0), + "blueviolet": (0.54117647058823526, 0.16862745098039217, 0.88627450980392153, 1.0), + "brown": (0.6470588235294118, 0.16470588235294117, 0.16470588235294117, 1.0), + "brown1": (1.0, 0.25098039215686274, 0.25098039215686274, 1.0), + "brown2": (0.93333333333333335, 0.23137254901960785, 0.23137254901960785, 1.0), + "brown3": (0.80392156862745101, 0.20000000000000001, 0.20000000000000001, 1.0), + "brown4": (0.54509803921568623, 0.13725490196078433, 0.13725490196078433, 1.0), + "burlywood": (0.87058823529411766, 0.72156862745098038, 0.52941176470588236, 1.0), + "burlywood1": (1.0, 0.82745098039215681, 0.60784313725490191, 1.0), + "burlywood2": (0.93333333333333335, 0.77254901960784317, 0.56862745098039214, 1.0), + "burlywood3": (0.80392156862745101, 0.66666666666666663, 0.49019607843137253, 1.0), + "burlywood4": (0.54509803921568623, 0.45098039215686275, 0.33333333333333331, 1.0), + "cadet blue": (0.37254901960784315, 0.61960784313725492, 0.62745098039215685, 1.0), + "cadetblue": (0.37254901960784315, 0.61960784313725492, 0.62745098039215685, 1.0), + "cadetblue1": (0.59607843137254901, 0.96078431372549022, 1.0, 1.0), + "cadetblue2": (0.55686274509803924, 0.89803921568627454, 0.93333333333333335, 1.0), + "cadetblue3": (0.47843137254901963, 0.77254901960784317, 0.80392156862745101, 1.0), + "cadetblue4": (0.32549019607843138, 0.52549019607843139, 0.54509803921568623, 1.0), + "chartreuse": (0.49803921568627452, 1.0, 0.0, 1.0), + "chartreuse1": (0.49803921568627452, 1.0, 0.0, 1.0), + "chartreuse2": (0.46274509803921571, 0.93333333333333335, 0.0, 1.0), + "chartreuse3": (0.40000000000000002, 0.80392156862745101, 0.0, 1.0), + "chartreuse4": (0.27058823529411763, 0.54509803921568623, 0.0, 1.0), + "chocolate": (0.82352941176470584, 0.41176470588235292, 0.11764705882352941, 1.0), + "chocolate1": (1.0, 0.49803921568627452, 0.14117647058823529, 1.0), + "chocolate2": (0.93333333333333335, 0.46274509803921571, 0.12941176470588237, 1.0), + "chocolate3": (0.80392156862745101, 0.40000000000000002, 0.11372549019607843, 1.0), + "chocolate4": (0.54509803921568623, 0.27058823529411763, 0.074509803921568626, 1.0), + "coral": (1.0, 0.49803921568627452, 0.31372549019607843, 1.0), + "coral1": (1.0, 0.44705882352941179, 0.33725490196078434, 1.0), + "coral2": (0.93333333333333335, 0.41568627450980394, 0.31372549019607843, 1.0), + "coral3": (0.80392156862745101, 0.35686274509803922, 0.27058823529411763, 1.0), + "coral4": (0.54509803921568623, 0.24313725490196078, 0.18431372549019609, 1.0), + "cornflower blue": ( + 0.39215686274509803, + 0.58431372549019611, + 0.92941176470588238, + 1.0, + ), + "cornflowerblue": ( + 0.39215686274509803, + 0.58431372549019611, + 0.92941176470588238, + 1.0, + ), + "cornsilk": (1.0, 0.97254901960784312, 0.86274509803921573, 1.0), + "cornsilk1": (1.0, 0.97254901960784312, 0.86274509803921573, 1.0), + "cornsilk2": (0.93333333333333335, 0.90980392156862744, 0.80392156862745101, 1.0), + "cornsilk3": (0.80392156862745101, 0.78431372549019607, 0.69411764705882351, 1.0), + "cornsilk4": (0.54509803921568623, 0.53333333333333333, 0.47058823529411764, 1.0), + "crimson": (0.8627450980392157, 0.0784313725490196, 0.23529411764705882, 1.0), + "cyan": (0.0, 1.0, 1.0, 1.0), + "cyan1": (0.0, 1.0, 1.0, 1.0), + "cyan2": (0.0, 0.93333333333333335, 0.93333333333333335, 1.0), + "cyan3": (0.0, 0.80392156862745101, 0.80392156862745101, 1.0), + "cyan4": (0.0, 0.54509803921568623, 0.54509803921568623, 1.0), + "dark blue": (0.0, 0.0, 0.54509803921568623, 1.0), + "dark cyan": (0.0, 0.54509803921568623, 0.54509803921568623, 1.0), + "dark goldenrod": ( + 0.72156862745098038, + 0.52549019607843139, + 0.043137254901960784, + 1.0, + ), + "dark gray": (0.66274509803921566, 0.66274509803921566, 0.66274509803921566, 1.0), + "dark green": (0.0, 0.39215686274509803, 0.0, 1.0), + "dark grey": (0.66274509803921566, 0.66274509803921566, 0.66274509803921566, 1.0), + "dark khaki": (0.74117647058823533, 0.71764705882352942, 0.41960784313725491, 1.0), + "dark magenta": (0.54509803921568623, 0.0, 0.54509803921568623, 1.0), + "dark olive green": ( + 0.33333333333333331, + 0.41960784313725491, + 0.18431372549019609, + 1.0, + ), + "dark orange": (1.0, 0.5490196078431373, 0.0, 1.0), + "dark orchid": (0.59999999999999998, 0.19607843137254902, 0.80000000000000004, 1.0), + "dark red": (0.54509803921568623, 0.0, 0.0, 1.0), + "dark salmon": (0.9137254901960784, 0.58823529411764708, 0.47843137254901963, 1.0), + "dark sea green": ( + 0.5607843137254902, + 0.73725490196078436, + 0.5607843137254902, + 1.0, + ), + "dark slate blue": ( + 0.28235294117647058, + 0.23921568627450981, + 0.54509803921568623, + 1.0, + ), + "dark slate gray": ( + 0.18431372549019609, + 0.30980392156862746, + 0.30980392156862746, + 1.0, + ), + "dark slate grey": ( + 0.18431372549019609, + 0.30980392156862746, + 0.30980392156862746, + 1.0, + ), + "dark turquoise": (0.0, 0.80784313725490198, 0.81960784313725488, 1.0), + "dark violet": (0.58039215686274515, 0.0, 0.82745098039215681, 1.0), + "darkblue": (0.0, 0.0, 0.54509803921568623, 1.0), + "darkcyan": (0.0, 0.54509803921568623, 0.54509803921568623, 1.0), + "darkgoldenrod": ( + 0.72156862745098038, + 0.52549019607843139, + 0.043137254901960784, + 1.0, + ), + "darkgoldenrod1": (1.0, 0.72549019607843135, 0.058823529411764705, 1.0), + "darkgoldenrod2": ( + 0.93333333333333335, + 0.67843137254901964, + 0.054901960784313725, + 1.0, + ), + "darkgoldenrod3": ( + 0.80392156862745101, + 0.58431372549019611, + 0.047058823529411764, + 1.0, + ), + "darkgoldenrod4": ( + 0.54509803921568623, + 0.396078431372549, + 0.031372549019607843, + 1.0, + ), + "darkgray": (0.66274509803921566, 0.66274509803921566, 0.66274509803921566, 1.0), + "darkgreen": (0.0, 0.39215686274509803, 0.0, 1.0), + "darkgrey": (0.66274509803921566, 0.66274509803921566, 0.66274509803921566, 1.0), + "darkkhaki": (0.74117647058823533, 0.71764705882352942, 0.41960784313725491, 1.0), + "darkmagenta": (0.54509803921568623, 0.0, 0.54509803921568623, 1.0), + "darkolivegreen": ( + 0.33333333333333331, + 0.41960784313725491, + 0.18431372549019609, + 1.0, + ), + "darkolivegreen1": (0.792156862745098, 1.0, 0.4392156862745098, 1.0), + "darkolivegreen2": ( + 0.73725490196078436, + 0.93333333333333335, + 0.40784313725490196, + 1.0, + ), + "darkolivegreen3": ( + 0.63529411764705879, + 0.80392156862745101, + 0.35294117647058826, + 1.0, + ), + "darkolivegreen4": ( + 0.43137254901960786, + 0.54509803921568623, + 0.23921568627450981, + 1.0, + ), + "darkorange": (1.0, 0.5490196078431373, 0.0, 1.0), + "darkorange1": (1.0, 0.49803921568627452, 0.0, 1.0), + "darkorange2": (0.93333333333333335, 0.46274509803921571, 0.0, 1.0), + "darkorange3": (0.80392156862745101, 0.40000000000000002, 0.0, 1.0), + "darkorange4": (0.54509803921568623, 0.27058823529411763, 0.0, 1.0), + "darkorchid": (0.59999999999999998, 0.19607843137254902, 0.80000000000000004, 1.0), + "darkorchid1": (0.74901960784313726, 0.24313725490196078, 1.0, 1.0), + "darkorchid2": (0.69803921568627447, 0.22745098039215686, 0.93333333333333335, 1.0), + "darkorchid3": (0.60392156862745094, 0.19607843137254902, 0.80392156862745101, 1.0), + "darkorchid4": (0.40784313725490196, 0.13333333333333333, 0.54509803921568623, 1.0), + "darkred": (0.54509803921568623, 0.0, 0.0, 1.0), + "darksalmon": (0.9137254901960784, 0.58823529411764708, 0.47843137254901963, 1.0), + "darkseagreen": (0.5607843137254902, 0.73725490196078436, 0.5607843137254902, 1.0), + "darkseagreen1": (0.75686274509803919, 1.0, 0.75686274509803919, 1.0), + "darkseagreen2": ( + 0.70588235294117652, + 0.93333333333333335, + 0.70588235294117652, + 1.0, + ), + "darkseagreen3": ( + 0.60784313725490191, + 0.80392156862745101, + 0.60784313725490191, + 1.0, + ), + "darkseagreen4": ( + 0.41176470588235292, + 0.54509803921568623, + 0.41176470588235292, + 1.0, + ), + "darkslateblue": ( + 0.28235294117647058, + 0.23921568627450981, + 0.54509803921568623, + 1.0, + ), + "darkslategray": ( + 0.18431372549019609, + 0.30980392156862746, + 0.30980392156862746, + 1.0, + ), + "darkslategray1": (0.59215686274509804, 1.0, 1.0, 1.0), + "darkslategray2": ( + 0.55294117647058827, + 0.93333333333333335, + 0.93333333333333335, + 1.0, + ), + "darkslategray3": ( + 0.47450980392156861, + 0.80392156862745101, + 0.80392156862745101, + 1.0, + ), + "darkslategray4": ( + 0.32156862745098042, + 0.54509803921568623, + 0.54509803921568623, + 1.0, + ), + "darkslategrey": ( + 0.18431372549019609, + 0.30980392156862746, + 0.30980392156862746, + 1.0, + ), + "darkturquoise": (0.0, 0.80784313725490198, 0.81960784313725488, 1.0), + "darkviolet": (0.58039215686274515, 0.0, 0.82745098039215681, 1.0), + "deep pink": (1.0, 0.078431372549019607, 0.57647058823529407, 1.0), + "deep sky blue": (0.0, 0.74901960784313726, 1.0, 1.0), + "deeppink": (1.0, 0.078431372549019607, 0.57647058823529407, 1.0), + "deeppink1": (1.0, 0.078431372549019607, 0.57647058823529407, 1.0), + "deeppink2": (0.93333333333333335, 0.070588235294117646, 0.53725490196078429, 1.0), + "deeppink3": (0.80392156862745101, 0.062745098039215685, 0.46274509803921571, 1.0), + "deeppink4": (0.54509803921568623, 0.039215686274509803, 0.31372549019607843, 1.0), + "deepskyblue": (0.0, 0.74901960784313726, 1.0, 1.0), + "deepskyblue1": (0.0, 0.74901960784313726, 1.0, 1.0), + "deepskyblue2": (0.0, 0.69803921568627447, 0.93333333333333335, 1.0), + "deepskyblue3": (0.0, 0.60392156862745094, 0.80392156862745101, 1.0), + "deepskyblue4": (0.0, 0.40784313725490196, 0.54509803921568623, 1.0), + "dim gray": (0.41176470588235292, 0.41176470588235292, 0.41176470588235292, 1.0), + "dim grey": (0.41176470588235292, 0.41176470588235292, 0.41176470588235292, 1.0), + "dimgray": (0.41176470588235292, 0.41176470588235292, 0.41176470588235292, 1.0), + "dimgrey": (0.41176470588235292, 0.41176470588235292, 0.41176470588235292, 1.0), + "dodger blue": (0.11764705882352941, 0.56470588235294117, 1.0, 1.0), + "dodgerblue": (0.11764705882352941, 0.56470588235294117, 1.0, 1.0), + "dodgerblue1": (0.11764705882352941, 0.56470588235294117, 1.0, 1.0), + "dodgerblue2": (0.10980392156862745, 0.52549019607843139, 0.93333333333333335, 1.0), + "dodgerblue3": ( + 0.094117647058823528, + 0.45490196078431372, + 0.80392156862745101, + 1.0, + ), + "dodgerblue4": ( + 0.062745098039215685, + 0.30588235294117649, + 0.54509803921568623, + 1.0, + ), + "firebrick": (0.69803921568627447, 0.13333333333333333, 0.13333333333333333, 1.0), + "firebrick1": (1.0, 0.18823529411764706, 0.18823529411764706, 1.0), + "firebrick2": (0.93333333333333335, 0.17254901960784313, 0.17254901960784313, 1.0), + "firebrick3": (0.80392156862745101, 0.14901960784313725, 0.14901960784313725, 1.0), + "firebrick4": (0.54509803921568623, 0.10196078431372549, 0.10196078431372549, 1.0), + "floral white": (1.0, 0.98039215686274506, 0.94117647058823528, 1.0), + "floralwhite": (1.0, 0.98039215686274506, 0.94117647058823528, 1.0), + "forest green": ( + 0.13333333333333333, + 0.54509803921568623, + 0.13333333333333333, + 1.0, + ), + "forestgreen": (0.13333333333333333, 0.54509803921568623, 0.13333333333333333, 1.0), + "fuchsia": (1.0, 0.0, 1.0, 1.0), + "gainsboro": (0.86274509803921573, 0.86274509803921573, 0.86274509803921573, 1.0), + "ghost white": (0.97254901960784312, 0.97254901960784312, 1.0, 1.0), + "ghostwhite": (0.97254901960784312, 0.97254901960784312, 1.0, 1.0), + "gold": (1.0, 0.84313725490196079, 0.0, 1.0), + "gold1": (1.0, 0.84313725490196079, 0.0, 1.0), + "gold2": (0.93333333333333335, 0.78823529411764703, 0.0, 1.0), + "gold3": (0.80392156862745101, 0.67843137254901964, 0.0, 1.0), + "gold4": (0.54509803921568623, 0.45882352941176469, 0.0, 1.0), + "goldenrod": (0.85490196078431369, 0.6470588235294118, 0.12549019607843137, 1.0), + "goldenrod1": (1.0, 0.75686274509803919, 0.14509803921568629, 1.0), + "goldenrod2": (0.93333333333333335, 0.70588235294117652, 0.13333333333333333, 1.0), + "goldenrod3": (0.80392156862745101, 0.60784313725490191, 0.11372549019607843, 1.0), + "goldenrod4": (0.54509803921568623, 0.41176470588235292, 0.078431372549019607, 1.0), + "gray": (0.74509803921568629, 0.74509803921568629, 0.74509803921568629, 1.0), + "gray0": (0.0, 0.0, 0.0, 1.0), + "gray1": (0.011764705882352941, 0.011764705882352941, 0.011764705882352941, 1.0), + "gray10": (0.10196078431372549, 0.10196078431372549, 0.10196078431372549, 1.0), + "gray100": (1.0, 1.0, 1.0, 1.0), + "gray11": (0.10980392156862745, 0.10980392156862745, 0.10980392156862745, 1.0), + "gray12": (0.12156862745098039, 0.12156862745098039, 0.12156862745098039, 1.0), + "gray13": (0.12941176470588237, 0.12941176470588237, 0.12941176470588237, 1.0), + "gray14": (0.14117647058823529, 0.14117647058823529, 0.14117647058823529, 1.0), + "gray15": (0.14901960784313725, 0.14901960784313725, 0.14901960784313725, 1.0), + "gray16": (0.16078431372549021, 0.16078431372549021, 0.16078431372549021, 1.0), + "gray17": (0.16862745098039217, 0.16862745098039217, 0.16862745098039217, 1.0), + "gray18": (0.1803921568627451, 0.1803921568627451, 0.1803921568627451, 1.0), + "gray19": (0.18823529411764706, 0.18823529411764706, 0.18823529411764706, 1.0), + "gray2": (0.019607843137254902, 0.019607843137254902, 0.019607843137254902, 1.0), + "gray20": (0.20000000000000001, 0.20000000000000001, 0.20000000000000001, 1.0), + "gray21": (0.21176470588235294, 0.21176470588235294, 0.21176470588235294, 1.0), + "gray22": (0.2196078431372549, 0.2196078431372549, 0.2196078431372549, 1.0), + "gray23": (0.23137254901960785, 0.23137254901960785, 0.23137254901960785, 1.0), + "gray24": (0.23921568627450981, 0.23921568627450981, 0.23921568627450981, 1.0), + "gray25": (0.25098039215686274, 0.25098039215686274, 0.25098039215686274, 1.0), + "gray26": (0.25882352941176473, 0.25882352941176473, 0.25882352941176473, 1.0), + "gray27": (0.27058823529411763, 0.27058823529411763, 0.27058823529411763, 1.0), + "gray28": (0.27843137254901962, 0.27843137254901962, 0.27843137254901962, 1.0), + "gray29": (0.29019607843137257, 0.29019607843137257, 0.29019607843137257, 1.0), + "gray3": (0.031372549019607843, 0.031372549019607843, 0.031372549019607843, 1.0), + "gray30": (0.30196078431372547, 0.30196078431372547, 0.30196078431372547, 1.0), + "gray31": (0.30980392156862746, 0.30980392156862746, 0.30980392156862746, 1.0), + "gray32": (0.32156862745098042, 0.32156862745098042, 0.32156862745098042, 1.0), + "gray33": (0.32941176470588235, 0.32941176470588235, 0.32941176470588235, 1.0), + "gray34": (0.3411764705882353, 0.3411764705882353, 0.3411764705882353, 1.0), + "gray35": (0.34901960784313724, 0.34901960784313724, 0.34901960784313724, 1.0), + "gray36": (0.36078431372549019, 0.36078431372549019, 0.36078431372549019, 1.0), + "gray37": (0.36862745098039218, 0.36862745098039218, 0.36862745098039218, 1.0), + "gray38": (0.38039215686274508, 0.38039215686274508, 0.38039215686274508, 1.0), + "gray39": (0.38823529411764707, 0.38823529411764707, 0.38823529411764707, 1.0), + "gray4": (0.039215686274509803, 0.039215686274509803, 0.039215686274509803, 1.0), + "gray40": (0.40000000000000002, 0.40000000000000002, 0.40000000000000002, 1.0), + "gray41": (0.41176470588235292, 0.41176470588235292, 0.41176470588235292, 1.0), + "gray42": (0.41960784313725491, 0.41960784313725491, 0.41960784313725491, 1.0), + "gray43": (0.43137254901960786, 0.43137254901960786, 0.43137254901960786, 1.0), + "gray44": (0.4392156862745098, 0.4392156862745098, 0.4392156862745098, 1.0), + "gray45": (0.45098039215686275, 0.45098039215686275, 0.45098039215686275, 1.0), + "gray46": (0.45882352941176469, 0.45882352941176469, 0.45882352941176469, 1.0), + "gray47": (0.47058823529411764, 0.47058823529411764, 0.47058823529411764, 1.0), + "gray48": (0.47843137254901963, 0.47843137254901963, 0.47843137254901963, 1.0), + "gray49": (0.49019607843137253, 0.49019607843137253, 0.49019607843137253, 1.0), + "gray5": (0.050980392156862744, 0.050980392156862744, 0.050980392156862744, 1.0), + "gray50": (0.49803921568627452, 0.49803921568627452, 0.49803921568627452, 1.0), + "gray51": (0.50980392156862742, 0.50980392156862742, 0.50980392156862742, 1.0), + "gray52": (0.52156862745098043, 0.52156862745098043, 0.52156862745098043, 1.0), + "gray53": (0.52941176470588236, 0.52941176470588236, 0.52941176470588236, 1.0), + "gray54": (0.54117647058823526, 0.54117647058823526, 0.54117647058823526, 1.0), + "gray55": (0.5490196078431373, 0.5490196078431373, 0.5490196078431373, 1.0), + "gray56": (0.5607843137254902, 0.5607843137254902, 0.5607843137254902, 1.0), + "gray57": (0.56862745098039214, 0.56862745098039214, 0.56862745098039214, 1.0), + "gray58": (0.58039215686274515, 0.58039215686274515, 0.58039215686274515, 1.0), + "gray59": (0.58823529411764708, 0.58823529411764708, 0.58823529411764708, 1.0), + "gray6": (0.058823529411764705, 0.058823529411764705, 0.058823529411764705, 1.0), + "gray60": (0.59999999999999998, 0.59999999999999998, 0.59999999999999998, 1.0), + "gray61": (0.61176470588235299, 0.61176470588235299, 0.61176470588235299, 1.0), + "gray62": (0.61960784313725492, 0.61960784313725492, 0.61960784313725492, 1.0), + "gray63": (0.63137254901960782, 0.63137254901960782, 0.63137254901960782, 1.0), + "gray64": (0.63921568627450975, 0.63921568627450975, 0.63921568627450975, 1.0), + "gray65": (0.65098039215686276, 0.65098039215686276, 0.65098039215686276, 1.0), + "gray66": (0.6588235294117647, 0.6588235294117647, 0.6588235294117647, 1.0), + "gray67": (0.6705882352941176, 0.6705882352941176, 0.6705882352941176, 1.0), + "gray68": (0.67843137254901964, 0.67843137254901964, 0.67843137254901964, 1.0), + "gray69": (0.69019607843137254, 0.69019607843137254, 0.69019607843137254, 1.0), + "gray7": (0.070588235294117646, 0.070588235294117646, 0.070588235294117646, 1.0), + "gray70": (0.70196078431372544, 0.70196078431372544, 0.70196078431372544, 1.0), + "gray71": (0.70980392156862748, 0.70980392156862748, 0.70980392156862748, 1.0), + "gray72": (0.72156862745098038, 0.72156862745098038, 0.72156862745098038, 1.0), + "gray73": (0.72941176470588232, 0.72941176470588232, 0.72941176470588232, 1.0), + "gray74": (0.74117647058823533, 0.74117647058823533, 0.74117647058823533, 1.0), + "gray75": (0.74901960784313726, 0.74901960784313726, 0.74901960784313726, 1.0), + "gray76": (0.76078431372549016, 0.76078431372549016, 0.76078431372549016, 1.0), + "gray77": (0.7686274509803922, 0.7686274509803922, 0.7686274509803922, 1.0), + "gray78": (0.7803921568627451, 0.7803921568627451, 0.7803921568627451, 1.0), + "gray79": (0.78823529411764703, 0.78823529411764703, 0.78823529411764703, 1.0), + "gray8": (0.078431372549019607, 0.078431372549019607, 0.078431372549019607, 1.0), + "gray80": (0.80000000000000004, 0.80000000000000004, 0.80000000000000004, 1.0), + "gray81": (0.81176470588235294, 0.81176470588235294, 0.81176470588235294, 1.0), + "gray82": (0.81960784313725488, 0.81960784313725488, 0.81960784313725488, 1.0), + "gray83": (0.83137254901960789, 0.83137254901960789, 0.83137254901960789, 1.0), + "gray84": (0.83921568627450982, 0.83921568627450982, 0.83921568627450982, 1.0), + "gray85": (0.85098039215686272, 0.85098039215686272, 0.85098039215686272, 1.0), + "gray86": (0.85882352941176465, 0.85882352941176465, 0.85882352941176465, 1.0), + "gray87": (0.87058823529411766, 0.87058823529411766, 0.87058823529411766, 1.0), + "gray88": (0.8784313725490196, 0.8784313725490196, 0.8784313725490196, 1.0), + "gray89": (0.8901960784313725, 0.8901960784313725, 0.8901960784313725, 1.0), + "gray9": (0.090196078431372548, 0.090196078431372548, 0.090196078431372548, 1.0), + "gray90": (0.89803921568627454, 0.89803921568627454, 0.89803921568627454, 1.0), + "gray91": (0.90980392156862744, 0.90980392156862744, 0.90980392156862744, 1.0), + "gray92": (0.92156862745098034, 0.92156862745098034, 0.92156862745098034, 1.0), + "gray93": (0.92941176470588238, 0.92941176470588238, 0.92941176470588238, 1.0), + "gray94": (0.94117647058823528, 0.94117647058823528, 0.94117647058823528, 1.0), + "gray95": (0.94901960784313721, 0.94901960784313721, 0.94901960784313721, 1.0), + "gray96": (0.96078431372549022, 0.96078431372549022, 0.96078431372549022, 1.0), + "gray97": (0.96862745098039216, 0.96862745098039216, 0.96862745098039216, 1.0), + "gray98": (0.98039215686274506, 0.98039215686274506, 0.98039215686274506, 1.0), + "gray99": (0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 1.0), + "green": (0.0, 1.0, 0.0, 1.0), + "green yellow": (0.67843137254901964, 1.0, 0.18431372549019609, 1.0), + "green1": (0.0, 1.0, 0.0, 1.0), + "green2": (0.0, 0.93333333333333335, 0.0, 1.0), + "green3": (0.0, 0.80392156862745101, 0.0, 1.0), + "green4": (0.0, 0.54509803921568623, 0.0, 1.0), + "greenyellow": (0.67843137254901964, 1.0, 0.18431372549019609, 1.0), + "grey": (0.74509803921568629, 0.74509803921568629, 0.74509803921568629, 1.0), + "grey0": (0.0, 0.0, 0.0, 1.0), + "grey1": (0.011764705882352941, 0.011764705882352941, 0.011764705882352941, 1.0), + "grey10": (0.10196078431372549, 0.10196078431372549, 0.10196078431372549, 1.0), + "grey100": (1.0, 1.0, 1.0, 1.0), + "grey11": (0.10980392156862745, 0.10980392156862745, 0.10980392156862745, 1.0), + "grey12": (0.12156862745098039, 0.12156862745098039, 0.12156862745098039, 1.0), + "grey13": (0.12941176470588237, 0.12941176470588237, 0.12941176470588237, 1.0), + "grey14": (0.14117647058823529, 0.14117647058823529, 0.14117647058823529, 1.0), + "grey15": (0.14901960784313725, 0.14901960784313725, 0.14901960784313725, 1.0), + "grey16": (0.16078431372549021, 0.16078431372549021, 0.16078431372549021, 1.0), + "grey17": (0.16862745098039217, 0.16862745098039217, 0.16862745098039217, 1.0), + "grey18": (0.1803921568627451, 0.1803921568627451, 0.1803921568627451, 1.0), + "grey19": (0.18823529411764706, 0.18823529411764706, 0.18823529411764706, 1.0), + "grey2": (0.019607843137254902, 0.019607843137254902, 0.019607843137254902, 1.0), + "grey20": (0.20000000000000001, 0.20000000000000001, 0.20000000000000001, 1.0), + "grey21": (0.21176470588235294, 0.21176470588235294, 0.21176470588235294, 1.0), + "grey22": (0.2196078431372549, 0.2196078431372549, 0.2196078431372549, 1.0), + "grey23": (0.23137254901960785, 0.23137254901960785, 0.23137254901960785, 1.0), + "grey24": (0.23921568627450981, 0.23921568627450981, 0.23921568627450981, 1.0), + "grey25": (0.25098039215686274, 0.25098039215686274, 0.25098039215686274, 1.0), + "grey26": (0.25882352941176473, 0.25882352941176473, 0.25882352941176473, 1.0), + "grey27": (0.27058823529411763, 0.27058823529411763, 0.27058823529411763, 1.0), + "grey28": (0.27843137254901962, 0.27843137254901962, 0.27843137254901962, 1.0), + "grey29": (0.29019607843137257, 0.29019607843137257, 0.29019607843137257, 1.0), + "grey3": (0.031372549019607843, 0.031372549019607843, 0.031372549019607843, 1.0), + "grey30": (0.30196078431372547, 0.30196078431372547, 0.30196078431372547, 1.0), + "grey31": (0.30980392156862746, 0.30980392156862746, 0.30980392156862746, 1.0), + "grey32": (0.32156862745098042, 0.32156862745098042, 0.32156862745098042, 1.0), + "grey33": (0.32941176470588235, 0.32941176470588235, 0.32941176470588235, 1.0), + "grey34": (0.3411764705882353, 0.3411764705882353, 0.3411764705882353, 1.0), + "grey35": (0.34901960784313724, 0.34901960784313724, 0.34901960784313724, 1.0), + "grey36": (0.36078431372549019, 0.36078431372549019, 0.36078431372549019, 1.0), + "grey37": (0.36862745098039218, 0.36862745098039218, 0.36862745098039218, 1.0), + "grey38": (0.38039215686274508, 0.38039215686274508, 0.38039215686274508, 1.0), + "grey39": (0.38823529411764707, 0.38823529411764707, 0.38823529411764707, 1.0), + "grey4": (0.039215686274509803, 0.039215686274509803, 0.039215686274509803, 1.0), + "grey40": (0.40000000000000002, 0.40000000000000002, 0.40000000000000002, 1.0), + "grey41": (0.41176470588235292, 0.41176470588235292, 0.41176470588235292, 1.0), + "grey42": (0.41960784313725491, 0.41960784313725491, 0.41960784313725491, 1.0), + "grey43": (0.43137254901960786, 0.43137254901960786, 0.43137254901960786, 1.0), + "grey44": (0.4392156862745098, 0.4392156862745098, 0.4392156862745098, 1.0), + "grey45": (0.45098039215686275, 0.45098039215686275, 0.45098039215686275, 1.0), + "grey46": (0.45882352941176469, 0.45882352941176469, 0.45882352941176469, 1.0), + "grey47": (0.47058823529411764, 0.47058823529411764, 0.47058823529411764, 1.0), + "grey48": (0.47843137254901963, 0.47843137254901963, 0.47843137254901963, 1.0), + "grey49": (0.49019607843137253, 0.49019607843137253, 0.49019607843137253, 1.0), + "grey5": (0.050980392156862744, 0.050980392156862744, 0.050980392156862744, 1.0), + "grey50": (0.49803921568627452, 0.49803921568627452, 0.49803921568627452, 1.0), + "grey51": (0.50980392156862742, 0.50980392156862742, 0.50980392156862742, 1.0), + "grey52": (0.52156862745098043, 0.52156862745098043, 0.52156862745098043, 1.0), + "grey53": (0.52941176470588236, 0.52941176470588236, 0.52941176470588236, 1.0), + "grey54": (0.54117647058823526, 0.54117647058823526, 0.54117647058823526, 1.0), + "grey55": (0.5490196078431373, 0.5490196078431373, 0.5490196078431373, 1.0), + "grey56": (0.5607843137254902, 0.5607843137254902, 0.5607843137254902, 1.0), + "grey57": (0.56862745098039214, 0.56862745098039214, 0.56862745098039214, 1.0), + "grey58": (0.58039215686274515, 0.58039215686274515, 0.58039215686274515, 1.0), + "grey59": (0.58823529411764708, 0.58823529411764708, 0.58823529411764708, 1.0), + "grey6": (0.058823529411764705, 0.058823529411764705, 0.058823529411764705, 1.0), + "grey60": (0.59999999999999998, 0.59999999999999998, 0.59999999999999998, 1.0), + "grey61": (0.61176470588235299, 0.61176470588235299, 0.61176470588235299, 1.0), + "grey62": (0.61960784313725492, 0.61960784313725492, 0.61960784313725492, 1.0), + "grey63": (0.63137254901960782, 0.63137254901960782, 0.63137254901960782, 1.0), + "grey64": (0.63921568627450975, 0.63921568627450975, 0.63921568627450975, 1.0), + "grey65": (0.65098039215686276, 0.65098039215686276, 0.65098039215686276, 1.0), + "grey66": (0.6588235294117647, 0.6588235294117647, 0.6588235294117647, 1.0), + "grey67": (0.6705882352941176, 0.6705882352941176, 0.6705882352941176, 1.0), + "grey68": (0.67843137254901964, 0.67843137254901964, 0.67843137254901964, 1.0), + "grey69": (0.69019607843137254, 0.69019607843137254, 0.69019607843137254, 1.0), + "grey7": (0.070588235294117646, 0.070588235294117646, 0.070588235294117646, 1.0), + "grey70": (0.70196078431372544, 0.70196078431372544, 0.70196078431372544, 1.0), + "grey71": (0.70980392156862748, 0.70980392156862748, 0.70980392156862748, 1.0), + "grey72": (0.72156862745098038, 0.72156862745098038, 0.72156862745098038, 1.0), + "grey73": (0.72941176470588232, 0.72941176470588232, 0.72941176470588232, 1.0), + "grey74": (0.74117647058823533, 0.74117647058823533, 0.74117647058823533, 1.0), + "grey75": (0.74901960784313726, 0.74901960784313726, 0.74901960784313726, 1.0), + "grey76": (0.76078431372549016, 0.76078431372549016, 0.76078431372549016, 1.0), + "grey77": (0.7686274509803922, 0.7686274509803922, 0.7686274509803922, 1.0), + "grey78": (0.7803921568627451, 0.7803921568627451, 0.7803921568627451, 1.0), + "grey79": (0.78823529411764703, 0.78823529411764703, 0.78823529411764703, 1.0), + "grey8": (0.078431372549019607, 0.078431372549019607, 0.078431372549019607, 1.0), + "grey80": (0.80000000000000004, 0.80000000000000004, 0.80000000000000004, 1.0), + "grey81": (0.81176470588235294, 0.81176470588235294, 0.81176470588235294, 1.0), + "grey82": (0.81960784313725488, 0.81960784313725488, 0.81960784313725488, 1.0), + "grey83": (0.83137254901960789, 0.83137254901960789, 0.83137254901960789, 1.0), + "grey84": (0.83921568627450982, 0.83921568627450982, 0.83921568627450982, 1.0), + "grey85": (0.85098039215686272, 0.85098039215686272, 0.85098039215686272, 1.0), + "grey86": (0.85882352941176465, 0.85882352941176465, 0.85882352941176465, 1.0), + "grey87": (0.87058823529411766, 0.87058823529411766, 0.87058823529411766, 1.0), + "grey88": (0.8784313725490196, 0.8784313725490196, 0.8784313725490196, 1.0), + "grey89": (0.8901960784313725, 0.8901960784313725, 0.8901960784313725, 1.0), + "grey9": (0.090196078431372548, 0.090196078431372548, 0.090196078431372548, 1.0), + "grey90": (0.89803921568627454, 0.89803921568627454, 0.89803921568627454, 1.0), + "grey91": (0.90980392156862744, 0.90980392156862744, 0.90980392156862744, 1.0), + "grey92": (0.92156862745098034, 0.92156862745098034, 0.92156862745098034, 1.0), + "grey93": (0.92941176470588238, 0.92941176470588238, 0.92941176470588238, 1.0), + "grey94": (0.94117647058823528, 0.94117647058823528, 0.94117647058823528, 1.0), + "grey95": (0.94901960784313721, 0.94901960784313721, 0.94901960784313721, 1.0), + "grey96": (0.96078431372549022, 0.96078431372549022, 0.96078431372549022, 1.0), + "grey97": (0.96862745098039216, 0.96862745098039216, 0.96862745098039216, 1.0), + "grey98": (0.98039215686274506, 0.98039215686274506, 0.98039215686274506, 1.0), + "grey99": (0.9882352941176471, 0.9882352941176471, 0.9882352941176471, 1.0), + "honeydew": (0.94117647058823528, 1.0, 0.94117647058823528, 1.0), + "honeydew1": (0.94117647058823528, 1.0, 0.94117647058823528, 1.0), + "honeydew2": (0.8784313725490196, 0.93333333333333335, 0.8784313725490196, 1.0), + "honeydew3": (0.75686274509803919, 0.80392156862745101, 0.75686274509803919, 1.0), + "honeydew4": (0.51372549019607838, 0.54509803921568623, 0.51372549019607838, 1.0), + "hot pink": (1.0, 0.41176470588235292, 0.70588235294117652, 1.0), + "hotpink": (1.0, 0.41176470588235292, 0.70588235294117652, 1.0), + "hotpink1": (1.0, 0.43137254901960786, 0.70588235294117652, 1.0), + "hotpink2": (0.93333333333333335, 0.41568627450980394, 0.65490196078431373, 1.0), + "hotpink3": (0.80392156862745101, 0.37647058823529411, 0.56470588235294117, 1.0), + "hotpink4": (0.54509803921568623, 0.22745098039215686, 0.3843137254901961, 1.0), + "indian red": (0.80392156862745101, 0.36078431372549019, 0.36078431372549019, 1.0), + "indianred": (0.80392156862745101, 0.36078431372549019, 0.36078431372549019, 1.0), + "indianred1": (1.0, 0.41568627450980394, 0.41568627450980394, 1.0), + "indianred2": (0.93333333333333335, 0.38823529411764707, 0.38823529411764707, 1.0), + "indianred3": (0.80392156862745101, 0.33333333333333331, 0.33333333333333331, 1.0), + "indianred4": (0.54509803921568623, 0.22745098039215686, 0.22745098039215686, 1.0), + "indigo": (0.29411764705882354, 0.0, 0.5098039215686274, 1.0), + "ivory": (1.0, 1.0, 0.94117647058823528, 1.0), + "ivory1": (1.0, 1.0, 0.94117647058823528, 1.0), + "ivory2": (0.93333333333333335, 0.93333333333333335, 0.8784313725490196, 1.0), + "ivory3": (0.80392156862745101, 0.80392156862745101, 0.75686274509803919, 1.0), + "ivory4": (0.54509803921568623, 0.54509803921568623, 0.51372549019607838, 1.0), + "khaki": (0.94117647058823528, 0.90196078431372551, 0.5490196078431373, 1.0), + "khaki1": (1.0, 0.96470588235294119, 0.5607843137254902, 1.0), + "khaki2": (0.93333333333333335, 0.90196078431372551, 0.52156862745098043, 1.0), + "khaki3": (0.80392156862745101, 0.77647058823529413, 0.45098039215686275, 1.0), + "khaki4": (0.54509803921568623, 0.52549019607843139, 0.30588235294117649, 1.0), + "lavender": (0.90196078431372551, 0.90196078431372551, 0.98039215686274506, 1.0), + "lavender blush": (1.0, 0.94117647058823528, 0.96078431372549022, 1.0), + "lavenderblush": (1.0, 0.94117647058823528, 0.96078431372549022, 1.0), + "lavenderblush1": (1.0, 0.94117647058823528, 0.96078431372549022, 1.0), + "lavenderblush2": ( + 0.93333333333333335, + 0.8784313725490196, + 0.89803921568627454, + 1.0, + ), + "lavenderblush3": ( + 0.80392156862745101, + 0.75686274509803919, + 0.77254901960784317, + 1.0, + ), + "lavenderblush4": ( + 0.54509803921568623, + 0.51372549019607838, + 0.52549019607843139, + 1.0, + ), + "lawn green": (0.48627450980392156, 0.9882352941176471, 0.0, 1.0), + "lawngreen": (0.48627450980392156, 0.9882352941176471, 0.0, 1.0), + "lemon chiffon": (1.0, 0.98039215686274506, 0.80392156862745101, 1.0), + "lemonchiffon": (1.0, 0.98039215686274506, 0.80392156862745101, 1.0), + "lemonchiffon1": (1.0, 0.98039215686274506, 0.80392156862745101, 1.0), + "lemonchiffon2": ( + 0.93333333333333335, + 0.9137254901960784, + 0.74901960784313726, + 1.0, + ), + "lemonchiffon3": ( + 0.80392156862745101, + 0.78823529411764703, + 0.6470588235294118, + 1.0, + ), + "lemonchiffon4": ( + 0.54509803921568623, + 0.53725490196078429, + 0.4392156862745098, + 1.0, + ), + "light blue": (0.67843137254901964, 0.84705882352941175, 0.90196078431372551, 1.0), + "light coral": (0.94117647058823528, 0.50196078431372548, 0.50196078431372548, 1.0), + "light cyan": (0.8784313725490196, 1.0, 1.0, 1.0), + "light goldenrod": ( + 0.93333333333333335, + 0.8666666666666667, + 0.50980392156862742, + 1.0, + ), + "light goldenrod yellow": ( + 0.98039215686274506, + 0.98039215686274506, + 0.82352941176470584, + 1.0, + ), + "light gray": (0.82745098039215681, 0.82745098039215681, 0.82745098039215681, 1.0), + "light green": (0.56470588235294117, 0.93333333333333335, 0.56470588235294117, 1.0), + "light grey": (0.82745098039215681, 0.82745098039215681, 0.82745098039215681, 1.0), + "light pink": (1.0, 0.71372549019607845, 0.75686274509803919, 1.0), + "light salmon": (1.0, 0.62745098039215685, 0.47843137254901963, 1.0), + "light sea green": ( + 0.12549019607843137, + 0.69803921568627447, + 0.66666666666666663, + 1.0, + ), + "light sky blue": ( + 0.52941176470588236, + 0.80784313725490198, + 0.98039215686274506, + 1.0, + ), + "light slate blue": (0.51764705882352946, 0.4392156862745098, 1.0, 1.0), + "light slate gray": ( + 0.46666666666666667, + 0.53333333333333333, + 0.59999999999999998, + 1.0, + ), + "light slate grey": ( + 0.46666666666666667, + 0.53333333333333333, + 0.59999999999999998, + 1.0, + ), + "light steel blue": ( + 0.69019607843137254, + 0.7686274509803922, + 0.87058823529411766, + 1.0, + ), + "light yellow": (1.0, 1.0, 0.8784313725490196, 1.0), + "lightblue": (0.67843137254901964, 0.84705882352941175, 0.90196078431372551, 1.0), + "lightblue1": (0.74901960784313726, 0.93725490196078431, 1.0, 1.0), + "lightblue2": (0.69803921568627447, 0.87450980392156863, 0.93333333333333335, 1.0), + "lightblue3": (0.60392156862745094, 0.75294117647058822, 0.80392156862745101, 1.0), + "lightblue4": (0.40784313725490196, 0.51372549019607838, 0.54509803921568623, 1.0), + "lightcoral": (0.94117647058823528, 0.50196078431372548, 0.50196078431372548, 1.0), + "lightcyan": (0.8784313725490196, 1.0, 1.0, 1.0), + "lightcyan1": (0.8784313725490196, 1.0, 1.0, 1.0), + "lightcyan2": (0.81960784313725488, 0.93333333333333335, 0.93333333333333335, 1.0), + "lightcyan3": (0.70588235294117652, 0.80392156862745101, 0.80392156862745101, 1.0), + "lightcyan4": (0.47843137254901963, 0.54509803921568623, 0.54509803921568623, 1.0), + "lightgoldenrod": ( + 0.93333333333333335, + 0.8666666666666667, + 0.50980392156862742, + 1.0, + ), + "lightgoldenrod1": (1.0, 0.92549019607843142, 0.54509803921568623, 1.0), + "lightgoldenrod2": ( + 0.93333333333333335, + 0.86274509803921573, + 0.50980392156862742, + 1.0, + ), + "lightgoldenrod3": ( + 0.80392156862745101, + 0.74509803921568629, + 0.4392156862745098, + 1.0, + ), + "lightgoldenrod4": ( + 0.54509803921568623, + 0.50588235294117645, + 0.29803921568627451, + 1.0, + ), + "lightgoldenrodyellow": ( + 0.98039215686274506, + 0.98039215686274506, + 0.82352941176470584, + 1.0, + ), + "lightgray": (0.82745098039215681, 0.82745098039215681, 0.82745098039215681, 1.0), + "lightgreen": (0.56470588235294117, 0.93333333333333335, 0.56470588235294117, 1.0), + "lightgrey": (0.82745098039215681, 0.82745098039215681, 0.82745098039215681, 1.0), + "lightpink": (1.0, 0.71372549019607845, 0.75686274509803919, 1.0), + "lightpink1": (1.0, 0.68235294117647061, 0.72549019607843135, 1.0), + "lightpink2": (0.93333333333333335, 0.63529411764705879, 0.67843137254901964, 1.0), + "lightpink3": (0.80392156862745101, 0.5490196078431373, 0.58431372549019611, 1.0), + "lightpink4": (0.54509803921568623, 0.37254901960784315, 0.396078431372549, 1.0), + "lightsalmon": (1.0, 0.62745098039215685, 0.47843137254901963, 1.0), + "lightsalmon1": (1.0, 0.62745098039215685, 0.47843137254901963, 1.0), + "lightsalmon2": ( + 0.93333333333333335, + 0.58431372549019611, + 0.44705882352941179, + 1.0, + ), + "lightsalmon3": (0.80392156862745101, 0.50588235294117645, 0.3843137254901961, 1.0), + "lightsalmon4": (0.54509803921568623, 0.3411764705882353, 0.25882352941176473, 1.0), + "lightseagreen": ( + 0.12549019607843137, + 0.69803921568627447, + 0.66666666666666663, + 1.0, + ), + "lightskyblue": ( + 0.52941176470588236, + 0.80784313725490198, + 0.98039215686274506, + 1.0, + ), + "lightskyblue1": (0.69019607843137254, 0.88627450980392153, 1.0, 1.0), + "lightskyblue2": ( + 0.64313725490196083, + 0.82745098039215681, + 0.93333333333333335, + 1.0, + ), + "lightskyblue3": ( + 0.55294117647058827, + 0.71372549019607845, + 0.80392156862745101, + 1.0, + ), + "lightskyblue4": ( + 0.37647058823529411, + 0.4823529411764706, + 0.54509803921568623, + 1.0, + ), + "lightslateblue": (0.51764705882352946, 0.4392156862745098, 1.0, 1.0), + "lightslategray": ( + 0.46666666666666667, + 0.53333333333333333, + 0.59999999999999998, + 1.0, + ), + "lightslategrey": ( + 0.46666666666666667, + 0.53333333333333333, + 0.59999999999999998, + 1.0, + ), + "lightsteelblue": ( + 0.69019607843137254, + 0.7686274509803922, + 0.87058823529411766, + 1.0, + ), + "lightsteelblue1": (0.792156862745098, 0.88235294117647056, 1.0, 1.0), + "lightsteelblue2": ( + 0.73725490196078436, + 0.82352941176470584, + 0.93333333333333335, + 1.0, + ), + "lightsteelblue3": ( + 0.63529411764705879, + 0.70980392156862748, + 0.80392156862745101, + 1.0, + ), + "lightsteelblue4": ( + 0.43137254901960786, + 0.4823529411764706, + 0.54509803921568623, + 1.0, + ), + "lightyellow": (1.0, 1.0, 0.8784313725490196, 1.0), + "lightyellow1": (1.0, 1.0, 0.8784313725490196, 1.0), + "lightyellow2": ( + 0.93333333333333335, + 0.93333333333333335, + 0.81960784313725488, + 1.0, + ), + "lightyellow3": ( + 0.80392156862745101, + 0.80392156862745101, + 0.70588235294117652, + 1.0, + ), + "lightyellow4": ( + 0.54509803921568623, + 0.54509803921568623, + 0.47843137254901963, + 1.0, + ), + "lime": (0.0, 1.0, 0.0, 1.0), + "lime green": (0.19607843137254902, 0.80392156862745101, 0.19607843137254902, 1.0), + "limegreen": (0.19607843137254902, 0.80392156862745101, 0.19607843137254902, 1.0), + "linen": (0.98039215686274506, 0.94117647058823528, 0.90196078431372551, 1.0), + "magenta": (1.0, 0.0, 1.0, 1.0), + "magenta1": (1.0, 0.0, 1.0, 1.0), + "magenta2": (0.93333333333333335, 0.0, 0.93333333333333335, 1.0), + "magenta3": (0.80392156862745101, 0.0, 0.80392156862745101, 1.0), + "magenta4": (0.54509803921568623, 0.0, 0.54509803921568623, 1.0), + "maroon": (0.69019607843137254, 0.18823529411764706, 0.37647058823529411, 1.0), + "maroon1": (1.0, 0.20392156862745098, 0.70196078431372544, 1.0), + "maroon2": (0.93333333333333335, 0.18823529411764706, 0.65490196078431373, 1.0), + "maroon3": (0.80392156862745101, 0.16078431372549021, 0.56470588235294117, 1.0), + "maroon4": (0.54509803921568623, 0.10980392156862745, 0.3843137254901961, 1.0), + "medium aquamarine": ( + 0.40000000000000002, + 0.80392156862745101, + 0.66666666666666663, + 1.0, + ), + "medium blue": (0.0, 0.0, 0.80392156862745101, 1.0), + "medium orchid": ( + 0.72941176470588232, + 0.33333333333333331, + 0.82745098039215681, + 1.0, + ), + "medium purple": ( + 0.57647058823529407, + 0.4392156862745098, + 0.85882352941176465, + 1.0, + ), + "medium sea green": ( + 0.23529411764705882, + 0.70196078431372544, + 0.44313725490196076, + 1.0, + ), + "medium slate blue": ( + 0.4823529411764706, + 0.40784313725490196, + 0.93333333333333335, + 1.0, + ), + "medium spring green": (0.0, 0.98039215686274506, 0.60392156862745094, 1.0), + "medium turquoise": ( + 0.28235294117647058, + 0.81960784313725488, + 0.80000000000000004, + 1.0, + ), + "medium violet red": ( + 0.7803921568627451, + 0.082352941176470587, + 0.52156862745098043, + 1.0, + ), + "mediumaquamarine": ( + 0.40000000000000002, + 0.80392156862745101, + 0.66666666666666663, + 1.0, + ), + "mediumblue": (0.0, 0.0, 0.80392156862745101, 1.0), + "mediumorchid": ( + 0.72941176470588232, + 0.33333333333333331, + 0.82745098039215681, + 1.0, + ), + "mediumorchid1": (0.8784313725490196, 0.40000000000000002, 1.0, 1.0), + "mediumorchid2": ( + 0.81960784313725488, + 0.37254901960784315, + 0.93333333333333335, + 1.0, + ), + "mediumorchid3": ( + 0.70588235294117652, + 0.32156862745098042, + 0.80392156862745101, + 1.0, + ), + "mediumorchid4": ( + 0.47843137254901963, + 0.21568627450980393, + 0.54509803921568623, + 1.0, + ), + "mediumpurple": (0.57647058823529407, 0.4392156862745098, 0.85882352941176465, 1.0), + "mediumpurple1": (0.6705882352941176, 0.50980392156862742, 1.0, 1.0), + "mediumpurple2": ( + 0.62352941176470589, + 0.47450980392156861, + 0.93333333333333335, + 1.0, + ), + "mediumpurple3": ( + 0.53725490196078429, + 0.40784313725490196, + 0.80392156862745101, + 1.0, + ), + "mediumpurple4": ( + 0.36470588235294116, + 0.27843137254901962, + 0.54509803921568623, + 1.0, + ), + "mediumseagreen": ( + 0.23529411764705882, + 0.70196078431372544, + 0.44313725490196076, + 1.0, + ), + "mediumslateblue": ( + 0.4823529411764706, + 0.40784313725490196, + 0.93333333333333335, + 1.0, + ), + "mediumspringgreen": (0.0, 0.98039215686274506, 0.60392156862745094, 1.0), + "mediumturquoise": ( + 0.28235294117647058, + 0.81960784313725488, + 0.80000000000000004, + 1.0, + ), + "mediumvioletred": ( + 0.7803921568627451, + 0.082352941176470587, + 0.52156862745098043, + 1.0, + ), + "midnight blue": ( + 0.098039215686274508, + 0.098039215686274508, + 0.4392156862745098, + 1.0, + ), + "midnightblue": ( + 0.098039215686274508, + 0.098039215686274508, + 0.4392156862745098, + 1.0, + ), + "mint cream": (0.96078431372549022, 1.0, 0.98039215686274506, 1.0), + "mintcream": (0.96078431372549022, 1.0, 0.98039215686274506, 1.0), + "misty rose": (1.0, 0.89411764705882357, 0.88235294117647056, 1.0), + "mistyrose": (1.0, 0.89411764705882357, 0.88235294117647056, 1.0), + "mistyrose1": (1.0, 0.89411764705882357, 0.88235294117647056, 1.0), + "mistyrose2": (0.93333333333333335, 0.83529411764705885, 0.82352941176470584, 1.0), + "mistyrose3": (0.80392156862745101, 0.71764705882352942, 0.70980392156862748, 1.0), + "mistyrose4": (0.54509803921568623, 0.49019607843137253, 0.4823529411764706, 1.0), + "moccasin": (1.0, 0.89411764705882357, 0.70980392156862748, 1.0), + "navajo white": (1.0, 0.87058823529411766, 0.67843137254901964, 1.0), + "navajowhite": (1.0, 0.87058823529411766, 0.67843137254901964, 1.0), + "navajowhite1": (1.0, 0.87058823529411766, 0.67843137254901964, 1.0), + "navajowhite2": ( + 0.93333333333333335, + 0.81176470588235294, + 0.63137254901960782, + 1.0, + ), + "navajowhite3": ( + 0.80392156862745101, + 0.70196078431372544, + 0.54509803921568623, + 1.0, + ), + "navajowhite4": ( + 0.54509803921568623, + 0.47450980392156861, + 0.36862745098039218, + 1.0, + ), + "navy": (0.0, 0.0, 0.50196078431372548, 1.0), + "navy blue": (0.0, 0.0, 0.50196078431372548, 1.0), + "navyblue": (0.0, 0.0, 0.50196078431372548, 1.0), + "old lace": (0.99215686274509807, 0.96078431372549022, 0.90196078431372551, 1.0), + "oldlace": (0.99215686274509807, 0.96078431372549022, 0.90196078431372551, 1.0), + "olive": (0.5, 0.5, 0.0, 1.0), + "olive drab": (0.41960784313725491, 0.55686274509803924, 0.13725490196078433, 1.0), + "olivedrab": (0.41960784313725491, 0.55686274509803924, 0.13725490196078433, 1.0), + "olivedrab1": (0.75294117647058822, 1.0, 0.24313725490196078, 1.0), + "olivedrab2": (0.70196078431372544, 0.93333333333333335, 0.22745098039215686, 1.0), + "olivedrab3": (0.60392156862745094, 0.80392156862745101, 0.19607843137254902, 1.0), + "olivedrab4": (0.41176470588235292, 0.54509803921568623, 0.13333333333333333, 1.0), + "orange": (1.0, 0.6470588235294118, 0.0, 1.0), + "orange red": (1.0, 0.27058823529411763, 0.0, 1.0), + "orange1": (1.0, 0.6470588235294118, 0.0, 1.0), + "orange2": (0.93333333333333335, 0.60392156862745094, 0.0, 1.0), + "orange3": (0.80392156862745101, 0.52156862745098043, 0.0, 1.0), + "orange4": (0.54509803921568623, 0.35294117647058826, 0.0, 1.0), + "orangered": (1.0, 0.27058823529411763, 0.0, 1.0), + "orangered1": (1.0, 0.27058823529411763, 0.0, 1.0), + "orangered2": (0.93333333333333335, 0.25098039215686274, 0.0, 1.0), + "orangered3": (0.80392156862745101, 0.21568627450980393, 0.0, 1.0), + "orangered4": (0.54509803921568623, 0.14509803921568629, 0.0, 1.0), + "orchid": (0.85490196078431369, 0.4392156862745098, 0.83921568627450982, 1.0), + "orchid1": (1.0, 0.51372549019607838, 0.98039215686274506, 1.0), + "orchid2": (0.93333333333333335, 0.47843137254901963, 0.9137254901960784, 1.0), + "orchid3": (0.80392156862745101, 0.41176470588235292, 0.78823529411764703, 1.0), + "orchid4": (0.54509803921568623, 0.27843137254901962, 0.53725490196078429, 1.0), + "pale goldenrod": ( + 0.93333333333333335, + 0.90980392156862744, + 0.66666666666666663, + 1.0, + ), + "pale green": (0.59607843137254901, 0.98431372549019602, 0.59607843137254901, 1.0), + "pale turquoise": ( + 0.68627450980392157, + 0.93333333333333335, + 0.93333333333333335, + 1.0, + ), + "pale violet red": ( + 0.85882352941176465, + 0.4392156862745098, + 0.57647058823529407, + 1.0, + ), + "palegoldenrod": ( + 0.93333333333333335, + 0.90980392156862744, + 0.66666666666666663, + 1.0, + ), + "palegreen": (0.59607843137254901, 0.98431372549019602, 0.59607843137254901, 1.0), + "palegreen1": (0.60392156862745094, 1.0, 0.60392156862745094, 1.0), + "palegreen2": (0.56470588235294117, 0.93333333333333335, 0.56470588235294117, 1.0), + "palegreen3": (0.48627450980392156, 0.80392156862745101, 0.48627450980392156, 1.0), + "palegreen4": (0.32941176470588235, 0.54509803921568623, 0.32941176470588235, 1.0), + "paleturquoise": ( + 0.68627450980392157, + 0.93333333333333335, + 0.93333333333333335, + 1.0, + ), + "paleturquoise1": (0.73333333333333328, 1.0, 1.0, 1.0), + "paleturquoise2": ( + 0.68235294117647061, + 0.93333333333333335, + 0.93333333333333335, + 1.0, + ), + "paleturquoise3": ( + 0.58823529411764708, + 0.80392156862745101, + 0.80392156862745101, + 1.0, + ), + "paleturquoise4": ( + 0.40000000000000002, + 0.54509803921568623, + 0.54509803921568623, + 1.0, + ), + "palevioletred": ( + 0.85882352941176465, + 0.4392156862745098, + 0.57647058823529407, + 1.0, + ), + "palevioletred1": (1.0, 0.50980392156862742, 0.6705882352941176, 1.0), + "palevioletred2": ( + 0.93333333333333335, + 0.47450980392156861, + 0.62352941176470589, + 1.0, + ), + "palevioletred3": ( + 0.80392156862745101, + 0.40784313725490196, + 0.53725490196078429, + 1.0, + ), + "palevioletred4": ( + 0.54509803921568623, + 0.27843137254901962, + 0.36470588235294116, + 1.0, + ), + "papaya whip": (1.0, 0.93725490196078431, 0.83529411764705885, 1.0), + "papayawhip": (1.0, 0.93725490196078431, 0.83529411764705885, 1.0), + "peach puff": (1.0, 0.85490196078431369, 0.72549019607843135, 1.0), + "peachpuff": (1.0, 0.85490196078431369, 0.72549019607843135, 1.0), + "peachpuff1": (1.0, 0.85490196078431369, 0.72549019607843135, 1.0), + "peachpuff2": (0.93333333333333335, 0.79607843137254897, 0.67843137254901964, 1.0), + "peachpuff3": (0.80392156862745101, 0.68627450980392157, 0.58431372549019611, 1.0), + "peachpuff4": (0.54509803921568623, 0.46666666666666667, 0.396078431372549, 1.0), + "peru": (0.80392156862745101, 0.52156862745098043, 0.24705882352941178, 1.0), + "pink": (1.0, 0.75294117647058822, 0.79607843137254897, 1.0), + "pink1": (1.0, 0.70980392156862748, 0.77254901960784317, 1.0), + "pink2": (0.93333333333333335, 0.66274509803921566, 0.72156862745098038, 1.0), + "pink3": (0.80392156862745101, 0.56862745098039214, 0.61960784313725492, 1.0), + "pink4": (0.54509803921568623, 0.38823529411764707, 0.42352941176470588, 1.0), + "plum": (0.8666666666666667, 0.62745098039215685, 0.8666666666666667, 1.0), + "plum1": (1.0, 0.73333333333333328, 1.0, 1.0), + "plum2": (0.93333333333333335, 0.68235294117647061, 0.93333333333333335, 1.0), + "plum3": (0.80392156862745101, 0.58823529411764708, 0.80392156862745101, 1.0), + "plum4": (0.54509803921568623, 0.40000000000000002, 0.54509803921568623, 1.0), + "powder blue": (0.69019607843137254, 0.8784313725490196, 0.90196078431372551, 1.0), + "powderblue": (0.69019607843137254, 0.8784313725490196, 0.90196078431372551, 1.0), + "purple": (0.62745098039215685, 0.12549019607843137, 0.94117647058823528, 1.0), + "purple1": (0.60784313725490191, 0.18823529411764706, 1.0, 1.0), + "purple2": (0.56862745098039214, 0.17254901960784313, 0.93333333333333335, 1.0), + "purple3": (0.49019607843137253, 0.14901960784313725, 0.80392156862745101, 1.0), + "purple4": (0.33333333333333331, 0.10196078431372549, 0.54509803921568623, 1.0), + "rebecca purple": (0.4, 0.2, 0.6, 1.0), + "rebeccapurple": (0.4, 0.2, 0.6, 1.0), + "red": (1.0, 0.0, 0.0, 1.0), + "red1": (1.0, 0.0, 0.0, 1.0), + "red2": (0.93333333333333335, 0.0, 0.0, 1.0), + "red3": (0.80392156862745101, 0.0, 0.0, 1.0), + "red4": (0.54509803921568623, 0.0, 0.0, 1.0), + "rosy brown": (0.73725490196078436, 0.5607843137254902, 0.5607843137254902, 1.0), + "rosybrown": (0.73725490196078436, 0.5607843137254902, 0.5607843137254902, 1.0), + "rosybrown1": (1.0, 0.75686274509803919, 0.75686274509803919, 1.0), + "rosybrown2": (0.93333333333333335, 0.70588235294117652, 0.70588235294117652, 1.0), + "rosybrown3": (0.80392156862745101, 0.60784313725490191, 0.60784313725490191, 1.0), + "rosybrown4": (0.54509803921568623, 0.41176470588235292, 0.41176470588235292, 1.0), + "royal blue": (0.25490196078431371, 0.41176470588235292, 0.88235294117647056, 1.0), + "royalblue": (0.25490196078431371, 0.41176470588235292, 0.88235294117647056, 1.0), + "royalblue1": (0.28235294117647058, 0.46274509803921571, 1.0, 1.0), + "royalblue2": (0.2627450980392157, 0.43137254901960786, 0.93333333333333335, 1.0), + "royalblue3": (0.22745098039215686, 0.37254901960784315, 0.80392156862745101, 1.0), + "royalblue4": (0.15294117647058825, 0.25098039215686274, 0.54509803921568623, 1.0), + "saddle brown": ( + 0.54509803921568623, + 0.27058823529411763, + 0.074509803921568626, + 1.0, + ), + "saddlebrown": ( + 0.54509803921568623, + 0.27058823529411763, + 0.074509803921568626, + 1.0, + ), + "salmon": (0.98039215686274506, 0.50196078431372548, 0.44705882352941179, 1.0), + "salmon1": (1.0, 0.5490196078431373, 0.41176470588235292, 1.0), + "salmon2": (0.93333333333333335, 0.50980392156862742, 0.3843137254901961, 1.0), + "salmon3": (0.80392156862745101, 0.4392156862745098, 0.32941176470588235, 1.0), + "salmon4": (0.54509803921568623, 0.29803921568627451, 0.22352941176470589, 1.0), + "sandy brown": (0.95686274509803926, 0.64313725490196083, 0.37647058823529411, 1.0), + "sandybrown": (0.95686274509803926, 0.64313725490196083, 0.37647058823529411, 1.0), + "sea green": (0.1803921568627451, 0.54509803921568623, 0.3411764705882353, 1.0), + "seagreen": (0.1803921568627451, 0.54509803921568623, 0.3411764705882353, 1.0), + "seagreen1": (0.32941176470588235, 1.0, 0.62352941176470589, 1.0), + "seagreen2": (0.30588235294117649, 0.93333333333333335, 0.58039215686274515, 1.0), + "seagreen3": (0.2627450980392157, 0.80392156862745101, 0.50196078431372548, 1.0), + "seagreen4": (0.1803921568627451, 0.54509803921568623, 0.3411764705882353, 1.0), + "seashell": (1.0, 0.96078431372549022, 0.93333333333333335, 1.0), + "seashell1": (1.0, 0.96078431372549022, 0.93333333333333335, 1.0), + "seashell2": (0.93333333333333335, 0.89803921568627454, 0.87058823529411766, 1.0), + "seashell3": (0.80392156862745101, 0.77254901960784317, 0.74901960784313726, 1.0), + "seashell4": (0.54509803921568623, 0.52549019607843139, 0.50980392156862742, 1.0), + "sienna": (0.62745098039215685, 0.32156862745098042, 0.17647058823529413, 1.0), + "sienna1": (1.0, 0.50980392156862742, 0.27843137254901962, 1.0), + "sienna2": (0.93333333333333335, 0.47450980392156861, 0.25882352941176473, 1.0), + "sienna3": (0.80392156862745101, 0.40784313725490196, 0.22352941176470589, 1.0), + "sienna4": (0.54509803921568623, 0.27843137254901962, 0.14901960784313725, 1.0), + "silver": (0.75, 0.75, 0.75, 1.0), + "sky blue": (0.52941176470588236, 0.80784313725490198, 0.92156862745098034, 1.0), + "skyblue": (0.52941176470588236, 0.80784313725490198, 0.92156862745098034, 1.0), + "skyblue1": (0.52941176470588236, 0.80784313725490198, 1.0, 1.0), + "skyblue2": (0.49411764705882355, 0.75294117647058822, 0.93333333333333335, 1.0), + "skyblue3": (0.42352941176470588, 0.65098039215686276, 0.80392156862745101, 1.0), + "skyblue4": (0.29019607843137257, 0.4392156862745098, 0.54509803921568623, 1.0), + "slate blue": (0.41568627450980394, 0.35294117647058826, 0.80392156862745101, 1.0), + "slate gray": (0.4392156862745098, 0.50196078431372548, 0.56470588235294117, 1.0), + "slate grey": (0.4392156862745098, 0.50196078431372548, 0.56470588235294117, 1.0), + "slateblue": (0.41568627450980394, 0.35294117647058826, 0.80392156862745101, 1.0), + "slateblue1": (0.51372549019607838, 0.43529411764705883, 1.0, 1.0), + "slateblue2": (0.47843137254901963, 0.40392156862745099, 0.93333333333333335, 1.0), + "slateblue3": (0.41176470588235292, 0.34901960784313724, 0.80392156862745101, 1.0), + "slateblue4": (0.27843137254901962, 0.23529411764705882, 0.54509803921568623, 1.0), + "slategray": (0.4392156862745098, 0.50196078431372548, 0.56470588235294117, 1.0), + "slategray1": (0.77647058823529413, 0.88627450980392153, 1.0, 1.0), + "slategray2": (0.72549019607843135, 0.82745098039215681, 0.93333333333333335, 1.0), + "slategray3": (0.62352941176470589, 0.71372549019607845, 0.80392156862745101, 1.0), + "slategray4": (0.42352941176470588, 0.4823529411764706, 0.54509803921568623, 1.0), + "slategrey": (0.4392156862745098, 0.50196078431372548, 0.56470588235294117, 1.0), + "snow": (1.0, 0.98039215686274506, 0.98039215686274506, 1.0), + "snow1": (1.0, 0.98039215686274506, 0.98039215686274506, 1.0), + "snow2": (0.93333333333333335, 0.9137254901960784, 0.9137254901960784, 1.0), + "snow3": (0.80392156862745101, 0.78823529411764703, 0.78823529411764703, 1.0), + "snow4": (0.54509803921568623, 0.53725490196078429, 0.53725490196078429, 1.0), + "spring green": (0.0, 1.0, 0.49803921568627452, 1.0), + "springgreen": (0.0, 1.0, 0.49803921568627452, 1.0), + "springgreen1": (0.0, 1.0, 0.49803921568627452, 1.0), + "springgreen2": (0.0, 0.93333333333333335, 0.46274509803921571, 1.0), + "springgreen3": (0.0, 0.80392156862745101, 0.40000000000000002, 1.0), + "springgreen4": (0.0, 0.54509803921568623, 0.27058823529411763, 1.0), + "steel blue": (0.27450980392156865, 0.50980392156862742, 0.70588235294117652, 1.0), + "steelblue": (0.27450980392156865, 0.50980392156862742, 0.70588235294117652, 1.0), + "steelblue1": (0.38823529411764707, 0.72156862745098038, 1.0, 1.0), + "steelblue2": (0.36078431372549019, 0.67450980392156867, 0.93333333333333335, 1.0), + "steelblue3": (0.30980392156862746, 0.58039215686274515, 0.80392156862745101, 1.0), + "steelblue4": (0.21176470588235294, 0.39215686274509803, 0.54509803921568623, 1.0), + "tan": (0.82352941176470584, 0.70588235294117652, 0.5490196078431373, 1.0), + "tan1": (1.0, 0.6470588235294118, 0.30980392156862746, 1.0), + "tan2": (0.93333333333333335, 0.60392156862745094, 0.28627450980392155, 1.0), + "tan3": (0.80392156862745101, 0.52156862745098043, 0.24705882352941178, 1.0), + "tan4": (0.54509803921568623, 0.35294117647058826, 0.16862745098039217, 1.0), + "teal": (0.0, 0.5, 0.5, 1.0), + "thistle": (0.84705882352941175, 0.74901960784313726, 0.84705882352941175, 1.0), + "thistle1": (1.0, 0.88235294117647056, 1.0, 1.0), + "thistle2": (0.93333333333333335, 0.82352941176470584, 0.93333333333333335, 1.0), + "thistle3": (0.80392156862745101, 0.70980392156862748, 0.80392156862745101, 1.0), + "thistle4": (0.54509803921568623, 0.4823529411764706, 0.54509803921568623, 1.0), + "tomato": (1.0, 0.38823529411764707, 0.27843137254901962, 1.0), + "tomato1": (1.0, 0.38823529411764707, 0.27843137254901962, 1.0), + "tomato2": (0.93333333333333335, 0.36078431372549019, 0.25882352941176473, 1.0), + "tomato3": (0.80392156862745101, 0.30980392156862746, 0.22352941176470589, 1.0), + "tomato4": (0.54509803921568623, 0.21176470588235294, 0.14901960784313725, 1.0), + "turquoise": (0.25098039215686274, 0.8784313725490196, 0.81568627450980391, 1.0), + "turquoise1": (0.0, 0.96078431372549022, 1.0, 1.0), + "turquoise2": (0.0, 0.89803921568627454, 0.93333333333333335, 1.0), + "turquoise3": (0.0, 0.77254901960784317, 0.80392156862745101, 1.0), + "turquoise4": (0.0, 0.52549019607843139, 0.54509803921568623, 1.0), + "violet": (0.93333333333333335, 0.50980392156862742, 0.93333333333333335, 1.0), + "violet red": (0.81568627450980391, 0.12549019607843137, 0.56470588235294117, 1.0), + "violetred": (0.81568627450980391, 0.12549019607843137, 0.56470588235294117, 1.0), + "violetred1": (1.0, 0.24313725490196078, 0.58823529411764708, 1.0), + "violetred2": (0.93333333333333335, 0.22745098039215686, 0.5490196078431373, 1.0), + "violetred3": (0.80392156862745101, 0.19607843137254902, 0.47058823529411764, 1.0), + "violetred4": (0.54509803921568623, 0.13333333333333333, 0.32156862745098042, 1.0), + "web gray": (0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1.0), + "webgray": (0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1.0), + "web green": (0.0, 0.5019607843137255, 0.0, 1.0), + "webgreen": (0.0, 0.5019607843137255, 0.0, 1.0), + "webgray": (0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1.0), + "web grey": (0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1.0), + "webgrey": (0.5019607843137255, 0.5019607843137255, 0.5019607843137255, 1.0), + "web maroon": (0.5019607843137255, 0.0, 0.0, 1.0), + "webmaroon": (0.5019607843137255, 0.0, 0.0, 1.0), + "web purple": (0.4980392156862745, 0.0, 0.4980392156862745, 1.0), + "webpurple": (0.4980392156862745, 0.0, 0.4980392156862745, 1.0), + "wheat": (0.96078431372549022, 0.87058823529411766, 0.70196078431372544, 1.0), + "wheat1": (1.0, 0.90588235294117647, 0.72941176470588232, 1.0), + "wheat2": (0.93333333333333335, 0.84705882352941175, 0.68235294117647061, 1.0), + "wheat3": (0.80392156862745101, 0.72941176470588232, 0.58823529411764708, 1.0), + "wheat4": (0.54509803921568623, 0.49411764705882355, 0.40000000000000002, 1.0), + "white": (1.0, 1.0, 1.0, 1.0), + "white smoke": (0.96078431372549022, 0.96078431372549022, 0.96078431372549022, 1.0), + "whitesmoke": (0.96078431372549022, 0.96078431372549022, 0.96078431372549022, 1.0), + "yellow": (1.0, 1.0, 0.0, 1.0), + "yellow green": ( + 0.60392156862745094, + 0.80392156862745101, + 0.19607843137254902, + 1.0, + ), + "yellow1": (1.0, 1.0, 0.0, 1.0), + "yellow2": (0.93333333333333335, 0.93333333333333335, 0.0, 1.0), + "yellow3": (0.80392156862745101, 0.80392156862745101, 0.0, 1.0), + "yellow4": (0.54509803921568623, 0.54509803921568623, 0.0, 1.0), + "yellowgreen": (0.60392156862745094, 0.80392156862745101, 0.19607843137254902, 1.0), +} palettes = { "gray": GradientPalette("black", "white"), @@ -3109,10 +2057,8 @@ def lighten(color, ratio=0.5): "red-yellow-green": AdvancedGradientPalette(["red", "yellow", "green"]), "red-black-green": AdvancedGradientPalette(["red", "black", "green"]), "rainbow": RainbowPalette(), - "heat": AdvancedGradientPalette(["red", "yellow", "white"], - indices=[0, 192, 255]), - "terrain": AdvancedGradientPalette(["hsv(120, 100%, 65%)", - "hsv(60, 100%, 90%)", "hsv(0, 0%, 95%)"]) + "heat": AdvancedGradientPalette(["red", "yellow", "white"], indices=[0, 192, 255]), + "terrain": AdvancedGradientPalette( + ["hsv(120, 100%, 65%)", "hsv(60, 100%, 90%)", "hsv(0, 0%, 95%)"] + ), } - - diff --git a/src/igraph/drawing/coord.py b/src/igraph/drawing/coord.py index 51253b76e..91db1a05a 100644 --- a/src/igraph/drawing/coord.py +++ b/src/igraph/drawing/coord.py @@ -10,6 +10,7 @@ ##################################################################### + # pylint: disable-msg=R0922 # R0922: Abstract class is only referenced 1 times class CoordinateSystem(AbstractCairoDrawer): @@ -42,7 +43,7 @@ def draw(self): def local_to_context(self, x, y): """Converts local coordinates to the context coordinate system (given by the bounding box). - + This method must be overridden in derived classes.""" raise NotImplementedError("abstract class") @@ -105,15 +106,17 @@ def draw(self): """Draws the coordinate system.""" # Draw the frame coords = self.bbox.coords - self.context.set_source_rgb(0., 0., 0.) + self.context.set_source_rgb(0.0, 0.0, 0.0) self.context.set_line_width(1) - self.context.rectangle(coords[0], coords[1], \ - coords[2]-coords[0], coords[3]-coords[1]) + self.context.rectangle( + coords[0], coords[1], coords[2] - coords[0], coords[3] - coords[1] + ) self.context.stroke() def local_to_context(self, x, y): """Converts local coordinates to the context coordinate system (given by the bounding box). """ - return (x-self._ox)*self._sx+self._ox2, self._oy2-(y-self._oy)*self._sy - + return (x - self._ox) * self._sx + self._ox2, self._oy2 - ( + y - self._oy + ) * self._sy diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 64e5ffc22..1afdfd0cc 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -2,9 +2,14 @@ Drawers for various edge styles in graph plots. """ -__all__ = ["AbstractEdgeDrawer", "AlphaVaryingEdgeDrawer", - "ArrowEdgeDrawer", "DarkToLightEdgeDrawer", - "LightToDarkEdgeDrawer", "TaperedEdgeDrawer"] +__all__ = [ + "AbstractEdgeDrawer", + "AlphaVaryingEdgeDrawer", + "ArrowEdgeDrawer", + "DarkToLightEdgeDrawer", + "LightToDarkEdgeDrawer", + "TaperedEdgeDrawer", +] __license__ = "GPL" @@ -16,6 +21,7 @@ cairo = find_cairo() + class AbstractEdgeDrawer(object): """Abstract edge drawer object from which all concrete edge drawer implementations are derived.""" @@ -44,19 +50,22 @@ def _curvature_to_float(value): def _construct_visual_edge_builder(self): """Construct the visual edge builder that will collect the visual attributes of an edge when it is being drawn.""" + class VisualEdgeBuilder(AttributeCollectorBase): """Builder that collects some visual properties of an edge for drawing""" + _kwds_prefix = "edge_" - arrow_size = 1.0 + arrow_size = 1.0 arrow_width = 1.0 - color = ("#444", self.palette.get) - curved = (0.0, self._curvature_to_float) - label = None + color = ("#444", self.palette.get) + curved = (0.0, self._curvature_to_float) + label = None label_color = ("black", self.palette.get) - label_size = 12.0 - font = 'sans-serif' - width = 1.0 + label_size = 12.0 + font = "sans-serif" + width = 1.0 + return VisualEdgeBuilder def draw_directed_edge(self, edge, src_vertex, dest_vertex): @@ -85,9 +94,9 @@ def draw_loop_edge(self, edge, vertex): ctx.set_source_rgba(*edge.color) ctx.set_line_width(edge.width) radius = vertex.size * 1.5 - center_x = vertex.position[0] + cos(pi/4) * radius / 2. - center_y = vertex.position[1] - sin(pi/4) * radius / 2. - ctx.arc(center_x, center_y, radius/2., 0, pi * 2) + center_x = vertex.position[0] + cos(pi / 4) * radius / 2.0 + center_y = vertex.position[1] - sin(pi / 4) * radius / 2.0 + ctx.arc(center_x, center_y, radius / 2.0, 0, pi * 2) ctx.stroke() def draw_undirected_edge(self, edge, src_vertex, dest_vertex): @@ -103,7 +112,7 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): @param dest_vertex: the target vertex. Visual properties are given again as attributes. """ - if src_vertex == dest_vertex: # TODO + if src_vertex == dest_vertex: # TODO return self.draw_loop_edge(edge, src_vertex) ctx = self.context @@ -113,10 +122,12 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): if edge.curved: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position - aux1 = (2*x1+x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ - (2*y1+y2) / 3.0 + edge.curved * 0.5 * (x2-x1) - aux2 = (x1+2*x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ - (y1+2*y2) / 3.0 + edge.curved * 0.5 * (x2-x1) + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], *dest_vertex.position) else: ctx.line_to(*dest_vertex.position) @@ -145,13 +156,15 @@ def get_label_position(self, edge, src_vertex, dest_vertex): dy = dest_vertex.position[1] - src_vertex.position[1] if dx != 0 or dy != 0: # Note that we use -dy because the Y axis points downwards - angle = atan2(-dy, dx) % (2*pi) + angle = atan2(-dy, dx) % (2 * pi) else: angle = None # Determine the midpoint - pos = ((src_vertex.position[0] + dest_vertex.position[0]) / 2., \ - (src_vertex.position[1] + dest_vertex.position[1]) / 2) + pos = ( + (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, + (src_vertex.position[1] + dest_vertex.position[1]) / 2, + ) # Determine the alignment based on the angle pi4 = pi / 4 @@ -159,14 +172,26 @@ def get_label_position(self, edge, src_vertex, dest_vertex): halign, valign = TextAlignment.CENTER, TextAlignment.CENTER else: index = int((angle / pi4) % 8) - halign = [TextAlignment.RIGHT, TextAlignment.RIGHT, - TextAlignment.RIGHT, TextAlignment.RIGHT, - TextAlignment.LEFT, TextAlignment.LEFT, - TextAlignment.LEFT, TextAlignment.LEFT][index] - valign = [TextAlignment.BOTTOM, TextAlignment.CENTER, - TextAlignment.CENTER, TextAlignment.TOP, - TextAlignment.TOP, TextAlignment.CENTER, - TextAlignment.CENTER, TextAlignment.BOTTOM][index] + halign = [ + TextAlignment.RIGHT, + TextAlignment.RIGHT, + TextAlignment.RIGHT, + TextAlignment.RIGHT, + TextAlignment.LEFT, + TextAlignment.LEFT, + TextAlignment.LEFT, + TextAlignment.LEFT, + ][index] + valign = [ + TextAlignment.BOTTOM, + TextAlignment.CENTER, + TextAlignment.CENTER, + TextAlignment.TOP, + TextAlignment.TOP, + TextAlignment.CENTER, + TextAlignment.CENTER, + TextAlignment.BOTTOM, + ][index] return pos, (halign, valign) @@ -177,62 +202,71 @@ class ArrowEdgeDrawer(AbstractEdgeDrawer): """ def draw_directed_edge(self, edge, src_vertex, dest_vertex): - if src_vertex == dest_vertex: # TODO + if src_vertex == dest_vertex: # TODO return self.draw_loop_edge(edge, src_vertex) ctx = self.context (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position (x_src, y_src), (x_dest, y_dest) = src_vertex.position, dest_vertex.position - - def bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t): - """ Computes the Bezier curve from point (x0,y0) to (x3,y3) + def bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t): + """Computes the Bezier curve from point (x0,y0) to (x3,y3) via control points (x1,y1) and (x2,y2) with parameter t. """ - xt = (1.0 - t) ** 3 * x0 + 3. *t * (1.0 - t) ** 2 * x1 + 3. * t**2 * (1. - t) * x2 + t**3 * x3 - yt = (1.0 - t) ** 3 * y0 + 3. *t * (1.0 - t) ** 2 * y1 + 3. * t**2 * (1. - t) * y2 + t**3 * y3 - return xt,yt - - def euclidean_distance(x1,y1,x2,y2): - """ Computes the Euclidean distance between points (x1,y1) and (x2,y2). - """ - return sqrt( (1.0*x1-x2) **2 + (1.0*y1-y2) **2 ) - - def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): - """ Binary search solver for finding the intersection of a Bezier curve + xt = ( + (1.0 - t) ** 3 * x0 + + 3.0 * t * (1.0 - t) ** 2 * x1 + + 3.0 * t ** 2 * (1.0 - t) * x2 + + t ** 3 * x3 + ) + yt = ( + (1.0 - t) ** 3 * y0 + + 3.0 * t * (1.0 - t) ** 2 * y1 + + 3.0 * t ** 2 * (1.0 - t) * y2 + + t ** 3 * y3 + ) + return xt, yt + + def euclidean_distance(x1, y1, x2, y2): + """Computes the Euclidean distance between points (x1,y1) and (x2,y2).""" + return sqrt((1.0 * x1 - x2) ** 2 + (1.0 * y1 - y2) ** 2) + + def intersect_bezier_circle( + x0, y0, x1, y1, x2, y2, x3, y3, radius, max_iter=10 + ): + """Binary search solver for finding the intersection of a Bezier curve and a circle centered at the curve's end point. Returns the x,y of the intersection point. TODO: implement safeguard to ensure convergence in ALL possible cases. """ precision = radius / 20.0 - source_target_distance = euclidean_distance(x0,y0,x3,y3) + source_target_distance = euclidean_distance(x0, y0, x3, y3) radius = float(radius) t0 = 1.0 t1 = 1.0 - radius / source_target_distance xt0, yt0 = x3, y3 - xt1, yt1 = bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) + xt1, yt1 = bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) distance_t0 = 0 - distance_t1 = euclidean_distance(x3,y3, xt1,yt1) + distance_t1 = euclidean_distance(x3, y3, xt1, yt1) counter = 0 while abs(distance_t1 - radius) > precision and counter < max_iter: - if ((distance_t1-radius) > 0) != ((distance_t0-radius) > 0): - t_new = (t0 + t1)/2.0 + if ((distance_t1 - radius) > 0) != ((distance_t0 - radius) > 0): + t_new = (t0 + t1) / 2.0 else: - if (abs(distance_t1 - radius) < abs(distance_t0 - radius)): + if abs(distance_t1 - radius) < abs(distance_t0 - radius): # If t1 gets us closer to the circumference step in the same direction - t_new = t1 + (t1 - t0)/ 2.0 + t_new = t1 + (t1 - t0) / 2.0 else: t_new = t1 - (t1 - t0) t_new = 1 if t_new > 1 else (0 if t_new < 0 else t_new) - t0,t1 = t1,t_new + t0, t1 = t1, t_new distance_t0 = distance_t1 - xt1, yt1 = bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) - distance_t1 = euclidean_distance(x3,y3, xt1,yt1) + xt1, yt1 = bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) + distance_t1 = euclidean_distance(x3, y3, xt1, yt1) counter += 1 - return bezier_cubic(x0,y0, x1,y1, x2,y2, x3,y3, t1) - + return bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) # Draw the edge ctx.set_source_rgba(*edge.color) @@ -241,10 +275,12 @@ def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): if edge.curved: # Calculate the curve - aux1 = (2*x1+x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ - (2*y1+y2) / 3.0 + edge.curved * 0.5 * (x2-x1) - aux2 = (x1+2*x2) / 3.0 - edge.curved * 0.5 * (y2-y1), \ - (y1+2*y2) / 3.0 + edge.curved * 0.5 * (x2-x1) + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) # Coordinates of the control points of the Bezier curve xc1, yc1 = aux1 @@ -252,38 +288,60 @@ def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): # Determine where the edge intersects the circumference of the # vertex shape: Tip of the arrow - x2, y2 = intersect_bezier_circle(x_src,y_src, xc1,yc1, xc2,yc2, x_dest,y_dest, dest_vertex.size/2.0) + x2, y2 = intersect_bezier_circle( + x_src, y_src, xc1, yc1, xc2, yc2, x_dest, y_dest, dest_vertex.size / 2.0 + ) # Calculate the arrow head coordinates - angle = atan2(y_dest - y2, x_dest - x2) # navid - arrow_size = 15. * edge.arrow_size - arrow_width = 10. / edge.arrow_width + angle = atan2(y_dest - y2, x_dest - x2) # navid + arrow_size = 15.0 * edge.arrow_size + arrow_width = 10.0 / edge.arrow_width aux_points = [ - (x2 - arrow_size * cos(angle - pi/arrow_width), - y2 - arrow_size * sin(angle - pi/arrow_width)), - (x2 - arrow_size * cos(angle + pi/arrow_width), - y2 - arrow_size * sin(angle + pi/arrow_width)), - ] + ( + x2 - arrow_size * cos(angle - pi / arrow_width), + y2 - arrow_size * sin(angle - pi / arrow_width), + ), + ( + x2 - arrow_size * cos(angle + pi / arrow_width), + y2 - arrow_size * sin(angle + pi / arrow_width), + ), + ] # Midpoint of the base of the arrow triangle - x_arrow_mid , y_arrow_mid = (aux_points [0][0] + aux_points [1][0]) / 2.0, (aux_points [0][1] + aux_points [1][1]) / 2.0 + x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( + aux_points[0][1] + aux_points[1][1] + ) / 2.0 # Vector representing the base of the arrow triangle - x_arrow_base_vec, y_arrow_base_vec = (aux_points [0][0] - aux_points [1][0]) , (aux_points [0][1] - aux_points [1][1]) + x_arrow_base_vec, y_arrow_base_vec = ( + aux_points[0][0] - aux_points[1][0] + ), (aux_points[0][1] - aux_points[1][1]) # Recalculate the curve such that it lands on the base of the arrow triangle - aux1 = (2*x_src+x_arrow_mid) / 3.0 - edge.curved * 0.5 * (y_arrow_mid-y_src), \ - (2*y_src+y_arrow_mid) / 3.0 + edge.curved * 0.5 * (x_arrow_mid-x_src) - aux2 = (x_src+2*x_arrow_mid) / 3.0 - edge.curved * 0.5 * (y_arrow_mid-y_src), \ - (y_src+2*y_arrow_mid) / 3.0 + edge.curved * 0.5 * (x_arrow_mid-x_src) + aux1 = (2 * x_src + x_arrow_mid) / 3.0 - edge.curved * 0.5 * ( + y_arrow_mid - y_src + ), (2 * y_src + y_arrow_mid) / 3.0 + edge.curved * 0.5 * ( + x_arrow_mid - x_src + ) + aux2 = (x_src + 2 * x_arrow_mid) / 3.0 - edge.curved * 0.5 * ( + y_arrow_mid - y_src + ), (y_src + 2 * y_arrow_mid) / 3.0 + edge.curved * 0.5 * ( + x_arrow_mid - x_src + ) # Offset the second control point (aux2) such that it falls precisely on the normal to the arrow base vector # Strictly speaking, offset_length is the offset length divided by the length of the arrow base vector. - offset_length = (x_arrow_mid - aux2[0]) * x_arrow_base_vec + (y_arrow_mid - aux2[1]) * y_arrow_base_vec - offset_length /= euclidean_distance(0,0, x_arrow_base_vec, y_arrow_base_vec) ** 2 - - aux2 = aux2[0] + x_arrow_base_vec * offset_length, \ - aux2[1] + y_arrow_base_vec * offset_length + offset_length = (x_arrow_mid - aux2[0]) * x_arrow_base_vec + ( + y_arrow_mid - aux2[1] + ) * y_arrow_base_vec + offset_length /= ( + euclidean_distance(0, 0, x_arrow_base_vec, y_arrow_base_vec) ** 2 + ) + + aux2 = ( + aux2[0] + x_arrow_base_vec * offset_length, + aux2[1] + y_arrow_base_vec * offset_length, + ) # Draw tthe curve from the first vertex to the midpoint of the base of the arrow head ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], x_arrow_mid, y_arrow_mid) @@ -291,21 +349,28 @@ def intersect_bezier_circle(x0,y0, x1,y1, x2,y2, x3,y3, radius, max_iter=10): # Determine where the edge intersects the circumference of the # vertex shape. x2, y2 = dest_vertex.shape.intersection_point( - x2, y2, x1, y1, dest_vertex.size) + x2, y2, x1, y1, dest_vertex.size + ) # Draw the arrowhead angle = atan2(y_dest - y2, x_dest - x2) - arrow_size = 15. * edge.arrow_size - arrow_width = 10. / edge.arrow_width + arrow_size = 15.0 * edge.arrow_size + arrow_width = 10.0 / edge.arrow_width aux_points = [ - (x2 - arrow_size * cos(angle - pi/arrow_width), - y2 - arrow_size * sin(angle - pi/arrow_width)), - (x2 - arrow_size * cos(angle + pi/arrow_width), - y2 - arrow_size * sin(angle + pi/arrow_width)), + ( + x2 - arrow_size * cos(angle - pi / arrow_width), + y2 - arrow_size * sin(angle - pi / arrow_width), + ), + ( + x2 - arrow_size * cos(angle + pi / arrow_width), + y2 - arrow_size * sin(angle + pi / arrow_width), + ), ] # Midpoint of the base of the arrow triangle - x_arrow_mid , y_arrow_mid = (aux_points [0][0] + aux_points [1][0]) / 2.0, (aux_points [0][1] + aux_points [1][1]) / 2.0 + x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( + aux_points[0][1] + aux_points[1][1] + ) / 2.0 # Draw the line ctx.line_to(x_arrow_mid, y_arrow_mid) @@ -327,15 +392,14 @@ class TaperedEdgeDrawer(AbstractEdgeDrawer): """ def draw_directed_edge(self, edge, src_vertex, dest_vertex): - if src_vertex == dest_vertex: # TODO + if src_vertex == dest_vertex: # TODO return self.draw_loop_edge(edge, src_vertex) # Determine where the edge intersects the circumference of the # destination vertex. src_pos, dest_pos = src_vertex.position, dest_vertex.position dest_pos = dest_vertex.shape.intersection_point( - dest_pos[0], dest_pos[1], src_pos[0], src_pos[1], - dest_vertex.size + dest_pos[0], dest_pos[1], src_pos[0], src_pos[1], dest_vertex.size ) ctx = self.context @@ -343,13 +407,17 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): # Draw the edge ctx.set_source_rgba(*edge.color) ctx.set_line_width(edge.width) - angle = atan2(dest_pos[1]-src_pos[1], dest_pos[0]-src_pos[0]) - arrow_size = src_vertex.size / 4. + angle = atan2(dest_pos[1] - src_pos[1], dest_pos[0] - src_pos[0]) + arrow_size = src_vertex.size / 4.0 aux_points = [ - (src_pos[0] + arrow_size * cos(angle + pi/2), - src_pos[1] + arrow_size * sin(angle + pi/2)), - (src_pos[0] + arrow_size * cos(angle - pi/2), - src_pos[1] + arrow_size * sin(angle - pi/2)) + ( + src_pos[0] + arrow_size * cos(angle + pi / 2), + src_pos[1] + arrow_size * sin(angle + pi / 2), + ), + ( + src_pos[0] + arrow_size * cos(angle - pi / 2), + src_pos[1] + arrow_size * sin(angle - pi / 2), + ), ] ctx.move_to(*dest_pos) ctx.line_to(*aux_points[0]) @@ -366,11 +434,11 @@ class AlphaVaryingEdgeDrawer(AbstractEdgeDrawer): def __init__(self, context, alpha_at_src, alpha_at_dest): super(AlphaVaryingEdgeDrawer, self).__init__(context) - self.alpha_at_src = (clamp(float(alpha_at_src), 0., 1.), ) - self.alpha_at_dest = (clamp(float(alpha_at_dest), 0., 1.), ) + self.alpha_at_src = (clamp(float(alpha_at_src), 0.0, 1.0),) + self.alpha_at_dest = (clamp(float(alpha_at_dest), 0.0, 1.0),) def draw_directed_edge(self, edge, src_vertex, dest_vertex): - if src_vertex == dest_vertex: # TODO + if src_vertex == dest_vertex: # TODO return self.draw_loop_edge(edge, src_vertex) src_pos, dest_pos = src_vertex.position, dest_vertex.position @@ -413,4 +481,3 @@ class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): def __init__(self, context): super(DarkToLightEdgeDrawer, self).__init__(context, 1.0, 0.0) - diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index f713d2b67..6d59b5b66 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -14,15 +14,17 @@ """ from collections import defaultdict -from itertools import izip + from math import atan2, cos, pi, sin, tan, sqrt from warnings import warn from igraph._igraph import convex_hull, VertexSeq -from igraph.compat import property from igraph.configuration import Configuration -from igraph.drawing.baseclasses import AbstractDrawer, AbstractCairoDrawer, \ - AbstractXMLRPCDrawer +from igraph.drawing.baseclasses import ( + AbstractDrawer, + AbstractCairoDrawer, + AbstractXMLRPCDrawer, +) from igraph.drawing.colors import color_to_html_format, color_name_to_rgb from igraph.drawing.edge import ArrowEdgeDrawer from igraph.drawing.text import TextAlignment, TextDrawer @@ -39,6 +41,7 @@ ##################################################################### + # pylint: disable-msg=R0903 # R0903: too few public methods class AbstractGraphDrawer(AbstractDrawer): @@ -52,7 +55,7 @@ def draw(self, graph, *args, **kwds): """Abstract method, must be implemented in derived classes.""" raise NotImplementedError("abstract class") - def ensure_layout(self, layout, graph = None): + def ensure_layout(self, layout, graph=None): """Helper method that ensures that I{layout} is an instance of L{Layout}. If it is not, the method will try to convert it to a L{Layout} according to the following rules: @@ -84,11 +87,12 @@ def ensure_layout(self, layout, graph = None): layout = Layout(layout) return layout + ##################################################################### + class AbstractCairoGraphDrawer(AbstractGraphDrawer, AbstractCairoDrawer): - """Abstract base class for graph drawers that draw on a Cairo canvas. - """ + """Abstract base class for graph drawers that draw on a Cairo canvas.""" def __init__(self, context, bbox): """Constructs the graph drawer and associates it to the given @@ -103,8 +107,10 @@ def __init__(self, context, bbox): AbstractCairoDrawer.__init__(self, context, bbox) AbstractGraphDrawer.__init__(self) + ##################################################################### + class DefaultGraphDrawer(AbstractCairoGraphDrawer): """Class implementing the default visualisation of a graph. @@ -116,10 +122,14 @@ class DefaultGraphDrawer(AbstractCairoGraphDrawer): See L{Graph.__plot__()} for the keyword arguments understood by this drawer.""" - def __init__(self, context, bbox, \ - vertex_drawer_factory = DefaultVertexDrawer, - edge_drawer_factory = ArrowEdgeDrawer, - label_drawer_factory = TextDrawer): + def __init__( + self, + context, + bbox, + vertex_drawer_factory=DefaultVertexDrawer, + edge_drawer_factory=ArrowEdgeDrawer, + label_drawer_factory=TextDrawer, + ): """Constructs the graph drawer and associates it to the given Cairo context and the given L{BoundingBox}. @@ -175,11 +185,12 @@ def _determine_edge_order(self, graph, kwds): reverse = False if isinstance(edge_order_by, tuple): edge_order_by, reverse = edge_order_by - if isinstance(reverse, basestring): + if isinstance(reverse, str): reverse = reverse.lower().startswith("desc") attrs = graph.es[edge_order_by] - edge_order = sorted(range(len(attrs)), key=attrs.__getitem__, - reverse=bool(reverse)) + edge_order = sorted( + list(range(len(attrs))), key=attrs.__getitem__, reverse=bool(reverse) + ) return edge_order @@ -203,11 +214,12 @@ def _determine_vertex_order(self, graph, kwds): reverse = False if isinstance(vertex_order_by, tuple): vertex_order_by, reverse = vertex_order_by - if isinstance(reverse, basestring): + if isinstance(reverse, str): reverse = reverse.lower().startswith("desc") attrs = graph.vs[vertex_order_by] - vertex_order = sorted(range(len(attrs)), key=attrs.__getitem__, - reverse=bool(reverse)) + vertex_order = sorted( + list(range(len(attrs))), key=attrs.__getitem__, reverse=bool(reverse) + ) return vertex_order @@ -229,7 +241,7 @@ def draw(self, graph, palette, *args, **kwds): margin = list(margin) except TypeError: margin = [margin] - while len(margin)<4: + while len(margin) < 4: margin.extend(margin) # Contract the drawing area by the margin and fit the layout @@ -239,10 +251,14 @@ def draw(self, graph, palette, *args, **kwds): # Decide whether we need to calculate the curvature of edges # automatically -- and calculate them if needed. autocurve = kwds.get("autocurve", None) - if autocurve or (autocurve is None and \ - "edge_curved" not in kwds and "curved" not in graph.edge_attributes() \ - and graph.ecount() < 10000): + if autocurve or ( + autocurve is None + and "edge_curved" not in kwds + and "curved" not in graph.edge_attributes() + and graph.ecount() < 10000 + ): from igraph import autocurve + default = kwds.get("edge_curved", 0) if default is True: default = 0.5 @@ -261,7 +277,7 @@ def draw(self, graph, palette, *args, **kwds): # Determine the order in which we will draw the vertices and edges vertex_order = self._determine_vertex_order(graph, kwds) - edge_order = self._determine_edge_order(graph, kwds) + edge_order = self._determine_edge_order(graph, kwds) # Draw the highlighted groups (if any) if "mark_groups" in kwds: @@ -275,18 +291,16 @@ def draw(self, graph, palette, *args, **kwds): if isinstance(mark_groups, dict): # Dictionary mapping vertex indices or tuples of vertex # indices to colors - group_iter = mark_groups.iteritems() + group_iter = iter(mark_groups.items()) elif isinstance(mark_groups, (VertexClustering, VertexCover)): # Vertex clustering - group_iter = ( - (group, color) for color, group in enumerate(mark_groups) - ) + group_iter = ((group, color) for color, group in enumerate(mark_groups)) elif hasattr(mark_groups, "__iter__"): # Lists, tuples, iterators etc group_iter = iter(mark_groups) else: # False - group_iter = {}.iteritems() + group_iter = iter({}.items()) # We will need a polygon drawer to draw the convex hulls polygon_drawer = PolygonDrawer(context, bbox) @@ -315,19 +329,21 @@ def draw(self, graph, palette, *args, **kwds): if len(polygon) == 2: # Expand the polygon (which is a flat line otherwise) a, b = Point(*polygon[0]), Point(*polygon[1]) - c = corner_radius * (a-b).normalized() + c = corner_radius * (a - b).normalized() n = Point(-c[1], c[0]) polygon = [a + n, b + n, b - c, b - n, a - n, a + c] else: # Expand the polygon around its center of mass - center = Point(*[sum(coords) / float(len(coords)) - for coords in zip(*polygon)]) - polygon = [Point(*point).towards(center, -corner_radius) - for point in polygon] + center = Point( + *[sum(coords) / float(len(coords)) for coords in zip(*polygon)] + ) + polygon = [ + Point(*point).towards(center, -corner_radius) + for point in polygon + ] # Draw the hull - context.set_source_rgba(color[0], color[1], color[2], - color[3]*0.25) + context.set_source_rgba(color[0], color[1], color[2], color[3] * 0.25) polygon_drawer.draw_path(polygon, corner_radius=corner_radius) context.fill_preserve() context.set_source_rgba(*color) @@ -337,7 +353,7 @@ def draw(self, graph, palette, *args, **kwds): es = graph.es if edge_order is None: # Default edge order - edge_coord_iter = izip(es, edge_builder) + edge_coord_iter = zip(es, edge_builder) else: # Specified edge order edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) @@ -356,11 +372,12 @@ def draw(self, graph, palette, *args, **kwds): vs = graph.vs if vertex_order is None: # Default vertex order - vertex_coord_iter = izip(vs, vertex_builder, layout) + vertex_coord_iter = zip(vs, vertex_builder, layout) else: # Specified vertex order - vertex_coord_iter = ((vs[i], vertex_builder[i], layout[i]) - for i in vertex_order) + vertex_coord_iter = ( + (vs[i], vertex_builder[i], layout[i]) for i in vertex_order + ) # Draw the vertices drawer_method = vertex_drawer.draw @@ -377,11 +394,10 @@ def draw(self, graph, palette, *args, **kwds): # Construct the iterator that we will use to draw the vertex labels if vertex_order is None: # Default vertex order - vertex_coord_iter = izip(vertex_builder, layout) + vertex_coord_iter = zip(vertex_builder, layout) else: # Specified vertex order - vertex_coord_iter = ((vertex_builder[i], layout[i]) - for i in vertex_order) + vertex_coord_iter = ((vertex_builder[i], layout[i]) for i in vertex_order) # Draw the vertex labels for vertex, coords in vertex_coord_iter: @@ -389,8 +405,9 @@ def draw(self, graph, palette, *args, **kwds): continue # Set the font family, size, color and text - context.select_font_face(vertex.font, \ - cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) + context.select_font_face( + vertex.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL + ) context.set_font_size(vertex.label_size) context.set_source_rgba(*vertex.label_color) label_drawer.text = vertex.label @@ -398,26 +415,26 @@ def draw(self, graph, palette, *args, **kwds): if vertex.label_dist: # Label is displaced from the center of the vertex. _, yb, w, h, _, _ = label_drawer.text_extents() - w, h = w/2.0, h/2.0 - radius = vertex.label_dist * vertex.size / 2. + w, h = w / 2.0, h / 2.0 + radius = vertex.label_dist * vertex.size / 2.0 # First we find the reference point that is at distance `radius' # from the vertex in the direction given by `label_angle'. # Then we place the label in a way that the line connecting the # center of the bounding box of the label with the center of the # vertex goes through the reference point and the reference # point lies exactly on the bounding box of the vertex. - alpha = vertex.label_angle % (2*pi) + alpha = vertex.label_angle % (2 * pi) cx = coords[0] + radius * cos(alpha) cy = coords[1] - radius * sin(alpha) # Now we have the reference point. We have to decide which side # of the label box will intersect with the line that connects # the center of the label with the center of the vertex. if w > 0: - beta = atan2(h, w) % (2*pi) + beta = atan2(h, w) % (2 * pi) else: - beta = pi/2. + beta = pi / 2.0 gamma = pi - beta - if alpha > 2*pi-beta or alpha <= beta: + if alpha > 2 * pi - beta or alpha <= beta: # Intersection at left edge of label cx += w cy -= tan(alpha) * w @@ -426,9 +443,9 @@ def draw(self, graph, palette, *args, **kwds): try: cx += h / tan(alpha) except: - pass # tan(alpha) == inf + pass # tan(alpha) == inf cy -= h - elif alpha > gamma and alpha <= gamma + 2*beta: + elif alpha > gamma and alpha <= gamma + 2 * beta: # Intersection at right edge of label cx -= w cy += tan(alpha) * w @@ -437,23 +454,27 @@ def draw(self, graph, palette, *args, **kwds): try: cx -= h / tan(alpha) except: - pass # tan(alpha) == inf + pass # tan(alpha) == inf cy += h # Draw the label - label_drawer.draw_at(cx-w, cy-h-yb, wrap=wrap) + label_drawer.draw_at(cx - w, cy - h - yb, wrap=wrap) else: # Label is exactly in the center of the vertex cx, cy = coords - half_size = vertex.size / 2. - label_drawer.bbox = (cx - half_size, cy - half_size, - cx + half_size, cy + half_size) + half_size = vertex.size / 2.0 + label_drawer.bbox = ( + cx - half_size, + cy - half_size, + cx + half_size, + cy + half_size, + ) label_drawer.draw(wrap=wrap) # Construct the iterator that we will use to draw the edge labels es = graph.es if edge_order is None: # Default edge order - edge_coord_iter = izip(es, edge_builder) + edge_coord_iter = zip(es, edge_builder) else: # Specified edge order edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) @@ -464,8 +485,9 @@ def draw(self, graph, palette, *args, **kwds): continue # Set the font family, size, color and text - context.select_font_face(visual_edge.font, \ - cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) + context.select_font_face( + visual_edge.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL + ) context.set_font_size(visual_edge.label_size) context.set_source_rgba(*visual_edge.label_color) label_drawer.text = visual_edge.label @@ -473,8 +495,9 @@ def draw(self, graph, palette, *args, **kwds): # Ask the edge drawer to propose an anchor point for the label src, dest = edge.tuple src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] - (x, y), (halign, valign) = \ - edge_drawer.get_label_position(edge, src_vertex, dest_vertex) + (x, y), (halign, valign) = edge_drawer.get_label_position( + edge, src_vertex, dest_vertex + ) # Measure the text _, yb, w, h, _, _ = label_drawer.text_extents() @@ -494,12 +517,13 @@ def draw(self, graph, palette, *args, **kwds): # Draw the edge label label_drawer.halign = halign label_drawer.valign = valign - label_drawer.bbox = (x-w, y-h, x+w, y+h) + label_drawer.bbox = (x - w, y - h, x + w, y + h) label_drawer.draw(wrap=wrap) ##################################################################### + class UbiGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): """Graph drawer that draws a given graph on an UbiGraph display using the XML-RPC API of UbiGraph. @@ -524,15 +548,8 @@ def __init__(self, url="http://localhost:20738/RPC2"): """Constructs an UbiGraph drawer using the display at the given URL.""" super(UbiGraphDrawer, self).__init__(url, "ubigraph") - self.vertex_defaults = dict( - color="#ff0000", - shape="cube", - size=1.0 - ) - self.edge_defaults = dict( - color="#ffffff", - width=1.0 - ) + self.vertex_defaults = dict(color="#ff0000", shape="cube", size=1.0) + self.edge_defaults = dict(color="#ffffff", width=1.0) def draw(self, graph, *args, **kwds): """Draws the given graph on an UbiGraph display. @@ -545,9 +562,9 @@ def draw(self, graph, *args, **kwds): if kwds.get("clear", True): display.clear() - for k, v in self.vertex_defaults.iteritems(): + for k, v in self.vertex_defaults.items(): display.set_vertex_style_attribute(0, k, str(v)) - for k, v in self.edge_defaults.iteritems(): + for k, v in self.edge_defaults.items(): display.set_edge_style_attribute(0, k, str(v)) # Custom color converter function @@ -557,14 +574,16 @@ def color_conv(color): # Construct the visual vertex/edge builders class VisualVertexBuilder(AttributeCollectorBase): """Collects some visual properties of a vertex for drawing""" + _kwds_prefix = "vertex_" color = (str(self.vertex_defaults["color"]), color_conv) label = None shape = str(self.vertex_defaults["shape"]) - size = float(self.vertex_defaults["size"]) + size = float(self.vertex_defaults["size"]) class VisualEdgeBuilder(AttributeCollectorBase): """Collects some visual properties of an edge for drawing""" + _kwds_prefix = "edge_" color = (str(self.edge_defaults["color"]), color_conv) label = None @@ -580,8 +599,10 @@ class VisualEdgeBuilder(AttributeCollectorBase): # Add the edges new_edge = display.new_edge - eids = [new_edge(vertex_ids[edge.source], vertex_ids[edge.target]) \ - for edge in graph.es] + eids = [ + new_edge(vertex_ids[edge.source], vertex_ids[edge.target]) + for edge in graph.es + ] # Add arrowheads if needed if graph.is_directed(): @@ -590,7 +611,7 @@ class VisualEdgeBuilder(AttributeCollectorBase): # Set the vertex attributes set_attr = display.set_vertex_attribute vertex_defaults = self.vertex_defaults - for vertex_id, vertex in izip(vertex_ids, vertex_builder): + for vertex_id, vertex in zip(vertex_ids, vertex_builder): if vertex.color != vertex_defaults["color"]: set_attr(vertex_id, "color", vertex.color) if vertex.label: @@ -603,7 +624,7 @@ class VisualEdgeBuilder(AttributeCollectorBase): # Set the edge attributes set_attr = display.set_edge_attribute edge_defaults = self.edge_defaults - for edge_id, edge in izip(eids, edge_builder): + for edge_id, edge in zip(eids, edge_builder): if edge.color != edge_defaults["color"]: set_attr(edge_id, "color", edge.color) if edge.label: @@ -611,8 +632,10 @@ class VisualEdgeBuilder(AttributeCollectorBase): if edge.width != edge_defaults["width"]: set_attr(edge_id, "width", str(edge.width)) + ##################################################################### + class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): """Graph drawer that sends/receives graphs to/from Cytoscape using CytoscapeRPC. @@ -643,8 +666,7 @@ def __init__(self, url="http://localhost:9000/Cytoscape"): super(CytoscapeGraphDrawer, self).__init__(url, "Cytoscape") self.network_id = None - def draw(self, graph, name="Network from igraph", create_view=True, - *args, **kwds): + def draw(self, graph, name="Network from igraph", create_view=True, *args, **kwds): """Sends the given graph to Cytoscape as a new network. @param name: the name of the network in Cytoscape. @@ -655,7 +677,7 @@ def draw(self, graph, name="Network from igraph", create_view=True, vertex attribute or a list specifying the identifiers, one for each node in the graph. The default is C{None}, which simply uses the vertex index for each vertex.""" - from xmlrpclib import Fault + from xmlrpc.client import Fault cy = self.service @@ -664,8 +686,10 @@ def draw(self, graph, name="Network from igraph", create_view=True, try: network_id = cy.createNetwork(name, False) except Fault: - warn("CytoscapeRPC too old, cannot create network without view." - " Consider upgrading CytoscapeRPC to use this feature.") + warn( + "CytoscapeRPC too old, cannot create network without view." + " Consider upgrading CytoscapeRPC to use this feature." + ) network_id = cy.createNetwork(name) else: network_id = cy.createNetwork(name) @@ -677,7 +701,7 @@ def draw(self, graph, name="Network from igraph", create_view=True, if isinstance(node_ids, str): node_ids = graph.vs[node_ids] else: - node_ids = range(graph.vcount()) + node_ids = list(range(graph.vcount())) node_ids = [str(identifier) for identifier in node_ids] cy.createNodes(network_id, node_ids) @@ -686,11 +710,13 @@ def draw(self, graph, name="Network from igraph", create_view=True, for v1, v2 in graph.get_edgelist(): edgelists[0].append(node_ids[v1]) edgelists[1].append(node_ids[v2]) - edge_ids = cy.createEdges(network_id, - edgelists[0], edgelists[1], - ["unknown"] * graph.ecount(), - [graph.is_directed()] * graph.ecount(), - False + edge_ids = cy.createEdges( + network_id, + edgelists[0], + edgelists[1], + ["unknown"] * graph.ecount(), + [graph.is_directed()] * graph.ecount(), + False, ) if "layout" in kwds: @@ -698,9 +724,8 @@ def draw(self, graph, name="Network from igraph", create_view=True, layout = self.ensure_layout(kwds["layout"], graph) size = 100 * graph.vcount() ** 0.5 layout.fit_into((size, size), keep_aspect_ratio=True) - layout.translate(-size/2., -size/2.) - cy.setNodesPositions(network_id, - node_ids, *zip(*list(layout))) + layout.translate(-size / 2.0, -size / 2.0) + cy.setNodesPositions(network_id, node_ids, *list(zip(*list(layout)))) else: # Ask Cytoscape to perform the default layout so the user can # at least see something in Cytoscape while the attributes are @@ -717,8 +742,9 @@ def draw(self, graph, name="Network from igraph", create_view=True, # Resolve type conflicts (if any) try: - while attr in attr_names and \ - cy.getNetworkAttributeType(attr) != cy_type: + while ( + attr in attr_names and cy.getNetworkAttributeType(attr) != cy_type + ): attr += "_" except Fault: # getNetworkAttributeType is not available in some older versions @@ -730,11 +756,9 @@ def draw(self, graph, name="Network from igraph", create_view=True, attr_names = set(cy.getNodeAttributeNames()) for attr in graph.vertex_attributes(): cy_type, values = self.infer_cytoscape_type(graph.vs[attr]) - values = dict(pair for pair in izip(node_ids, values) - if pair[1] is not None) + values = dict(pair for pair in zip(node_ids, values) if pair[1] is not None) # Resolve type conflicts (if any) - while attr in attr_names and \ - cy.getNodeAttributeType(attr) != cy_type: + while attr in attr_names and cy.getNodeAttributeType(attr) != cy_type: attr += "_" # Send the attribute values cy.addNodeAttributes(attr, cy_type, values, True) @@ -743,16 +767,14 @@ def draw(self, graph, name="Network from igraph", create_view=True, attr_names = set(cy.getEdgeAttributeNames()) for attr in graph.edge_attributes(): cy_type, values = self.infer_cytoscape_type(graph.es[attr]) - values = dict(pair for pair in izip(edge_ids, values) - if pair[1] is not None) + values = dict(pair for pair in zip(edge_ids, values) if pair[1] is not None) # Resolve type conflicts (if any) - while attr in attr_names and \ - cy.getEdgeAttributeType(attr) != cy_type: + while attr in attr_names and cy.getEdgeAttributeType(attr) != cy_type: attr += "_" # Send the attribute values cy.addEdgeAttributes(attr, cy_type, values) - def fetch(self, name = None, directed = False, keep_canonical_names = False): + def fetch(self, name=None, directed=False, keep_canonical_names=False): """Fetches the network with the given name from Cytoscape. When fetching networks from Cytoscape, the C{canonicalName} attributes @@ -774,15 +796,15 @@ def fetch(self, name = None, directed = False, keep_canonical_names = False): version = version.split(" ")[0] version = tuple(map(int, version.split(".")[:2])) if version < (1, 3): - raise NotImplementedError("CytoscapeGraphDrawer requires " - "Cytoscape-RPC 1.3 or newer") + raise NotImplementedError( + "CytoscapeGraphDrawer requires " "Cytoscape-RPC 1.3 or newer" + ) # Find out the ID of the network we are interested in if name is None: network_id = cy.getNetworkID() else: - network_id = [k for k, v in cy.getNetworkList().iteritems() - if v == name] + network_id = [k for k, v in cy.getNetworkList().items() if v == name] if not network_id: raise ValueError("no such network: %r" % name) elif len(network_id) > 1: @@ -805,11 +827,11 @@ def fetch(self, name = None, directed = False, keep_canonical_names = False): continue has_attr = cy.nodesHaveAttribute(attr_name, vertices) filtered = [idx for idx, ok in enumerate(has_attr) if ok] - values = cy.getNodesAttributes(attr_name, - [name for name, ok in izip(vertices, has_attr) if ok] + values = cy.getNodesAttributes( + attr_name, [name for name, ok in zip(vertices, has_attr) if ok] ) attrs = [None] * n - for idx, value in izip(filtered, values): + for idx, value in zip(filtered, values): attrs[idx] = value vertex_attrs[attr_name] = attrs @@ -821,11 +843,11 @@ def fetch(self, name = None, directed = False, keep_canonical_names = False): continue has_attr = cy.edgesHaveAttribute(attr_name, edges) filtered = [idx for idx, ok in enumerate(has_attr) if ok] - values = cy.getEdgesAttributes(attr_name, - [name for name, ok in izip(edges, has_attr) if ok] + values = cy.getEdgesAttributes( + attr_name, [name for name, ok in zip(edges, has_attr) if ok] ) attrs = [None] * m - for idx, value in izip(filtered, values): + for idx, value in zip(filtered, values): attrs[idx] = value edge_attrs[attr_name] = attrs @@ -840,9 +862,14 @@ def fetch(self, name = None, directed = False, keep_canonical_names = False): edge_list.append((vertex_name_index[parts[0]], vertex_name_index[parts[2]])) del edges - return Graph(n, edge_list, directed=directed, - graph_attrs=graph_attrs, vertex_attrs=vertex_attrs, - edge_attrs=edge_attrs) + return Graph( + n, + edge_list, + directed=directed, + graph_attrs=graph_attrs, + vertex_attrs=vertex_attrs, + edge_attrs=edge_attrs, + ) @staticmethod def infer_cytoscape_type(values): @@ -858,17 +885,18 @@ def infer_cytoscape_type(values): types = [type(value) for value in values if value is not None] if all(t == bool for t in types): return "BOOLEAN", values - if all(issubclass(t, (int, long)) for t in types): + if all(issubclass(t, (int, int)) for t in types): return "INTEGER", values if all(issubclass(t, float) for t in types): return "FLOATING", values return "STRING", [ - str(value) if not isinstance(value, basestring) else value - for value in values + str(value) if not isinstance(value, str) else value for value in values ] + ##################################################################### + class GephiGraphStreamingDrawer(AbstractGraphDrawer): """Graph drawer that sends a graph to a file-like object (e.g., socket, URL connection, file) using the Gephi graph streaming format. @@ -913,6 +941,7 @@ def __init__(self, conn=None, *args, **kwds): super(GephiGraphStreamingDrawer, self).__init__() from igraph.remote.gephi import GephiGraphStreamer, GephiConnection + self.connection = conn or GephiConnection(*args, **kwds) self.streamer = GephiGraphStreamer() @@ -927,21 +956,21 @@ def draw(self, graph, *args, **kwds): """ self.streamer.post(graph, self.connection, encoder=kwds.get("encoder")) + ##################################################################### + class MatplotlibGraphDrawer(AbstractGraphDrawer): """Graph drawer that uses a pyplot.Axes as context""" _shape_dict = { - 'rectangle': 's', - 'circle': 'o', - 'hidden': 'none', - 'triangle-up': '^', - 'triangle-down': 'v', + "rectangle": "s", + "circle": "o", + "hidden": "none", + "triangle-up": "^", + "triangle-down": "v", } - - def __init__(self, ax): """Constructs the graph drawer and associates it with the mpl axes""" self.ax = ax @@ -956,9 +985,9 @@ def draw(self, graph, *args, **kwds): import numpy as np def shrink_vertex(ax, aux, vcoord, vsize_squared): - '''Shrink edge by vertex size''' + """Shrink edge by vertex size""" aux_display, vcoord_display = ax.transData.transform([aux, vcoord]) - d = sqrt(((aux_display - vcoord_display)**2).sum()) + d = sqrt(((aux_display - vcoord_display) ** 2).sum()) fr = sqrt(vsize_squared) / d end_display = vcoord_display + fr * (aux_display - vcoord_display) end = ax.transData.inverted().transform(end_display) @@ -982,7 +1011,7 @@ def callback_edge_offset(event): # FIXME: deal with unnamed *args # Get layout - layout = kwds.get('layout', graph.layout()) + layout = kwds.get("layout", graph.layout()) if isinstance(layout, str): layout = graph.layout(layout) @@ -993,7 +1022,7 @@ def callback_edge_offset(event): nv = graph.vcount() # Vertex size - vsizes = kwds.get('vertex_size', 5) + vsizes = kwds.get("vertex_size", 5) # Enforce numpy array for sizes, because (1) we need the square and (2) # they are needed to calculate autoshrinking of edges if np.isscalar(vsizes): @@ -1004,24 +1033,24 @@ def callback_edge_offset(event): vsizes **= 2 # Vertex color - c = kwds.get('vertex_color', 'steelblue') + c = kwds.get("vertex_color", "steelblue") # Vertex opacity - alpha = kwds.get('alpha', 1.0) + alpha = kwds.get("alpha", 1.0) # Vertex labels - label = kwds.get('vertex_label', None) + label = kwds.get("vertex_label", None) # Vertex label size - label_size = kwds.get('vertex_label_size', mpl.rcParams['font.size']) + label_size = kwds.get("vertex_label_size", mpl.rcParams["font.size"]) # Vertex zorder - vzorder = kwds.get('vertex_order', 2) + vzorder = kwds.get("vertex_order", 2) # Vertex shapes # mpl shapes use slightly different names from Cairo, but we want the # API to feel consistent, so we use a conversion dictionary - shapes = kwds.get('vertex_shape', 'o') + shapes = kwds.get("vertex_shape", "o") if shapes is not None: if isinstance(shapes, str): shapes = self._shape_dict.get(shapes, shapes) @@ -1038,19 +1067,19 @@ def callback_edge_offset(event): xi, yi = x[i], y[i] ax.text(xi, yi, lab, fontsize=label_size) - dx = (max(x) - min(x)) - dy = (max(y) - min(y)) + dx = max(x) - min(x) + dy = max(y) - min(y) ax.set_xlim(min(x) - 0.05 * dx, max(x) + 0.05 * dx) ax.set_ylim(min(y) - 0.05 * dy, max(y) + 0.05 * dy) # Edge properties ne = graph.ecount() - ec = kwds.get('edge_color', 'black') - edge_width = kwds.get('edge_width', 1) - arrow_width = kwds.get('edge_arrow_width', 2) - arrow_length = kwds.get('edge_arrow_size', 4) - ealpha = kwds.get('edge_alpha', 1.0) - ezorder = kwds.get('edge_order', 1.0) + ec = kwds.get("edge_color", "black") + edge_width = kwds.get("edge_width", 1) + arrow_width = kwds.get("edge_arrow_width", 2) + arrow_length = kwds.get("edge_arrow_size", 4) + ealpha = kwds.get("edge_alpha", 1.0) + ezorder = kwds.get("edge_order", 1.0) try: ezorder = float(ezorder) ezorder = [ezorder] * ne @@ -1060,10 +1089,14 @@ def callback_edge_offset(event): # Decide whether we need to calculate the curvature of edges # automatically -- and calculate them if needed. autocurve = kwds.get("autocurve", None) - if autocurve or (autocurve is None and \ - "edge_curved" not in kwds and "curved" not in graph.edge_attributes() \ - and graph.ecount() < 10000): + if autocurve or ( + autocurve is None + and "edge_curved" not in kwds + and "curved" not in graph.edge_attributes() + and graph.ecount() < 10000 + ): from igraph import autocurve + default = kwds.get("edge_curved", 0) if default is True: default = 0.5 @@ -1073,14 +1106,16 @@ def callback_edge_offset(event): # Arrow style for directed and undirected graphs if graph.is_directed(): arrowstyle = ArrowStyle( - '-|>', head_length=arrow_length, head_width=arrow_width, + "-|>", + head_length=arrow_length, + head_width=arrow_width, ) else: - arrowstyle = '-' + arrowstyle = "-" # Edge coordinates and curvature nloops = [0 for x in range(ne)] - has_curved = 'curved' in graph.es.attributes() + has_curved = "curved" in graph.es.attributes() arrows = [] for ie, edge in enumerate(graph.es): src, tgt = edge.source, edge.target @@ -1097,7 +1132,7 @@ def callback_edge_offset(event): nloopstot += 1 continue xn, yn = vcoord[tgtn] - angles.append(180. / pi * atan2(yn - y1, xn - x1) % 360) + angles.append(180.0 / pi * atan2(yn - y1, xn - x1) % 360) # with .neighbors(mode=ALL), which is default, loops are double # counted nloopstot //= 2 @@ -1110,9 +1145,11 @@ def callback_edge_offset(event): # Only one self loop, use a quadrant only angles = [(ashift + 135) % 360, (ashift + 225) % 360] else: - nshift = 360. / nloopstot - angles = [(ashift + nshift * nloops[src]) % 360, - (ashift + nshift * (nloops[src] + 1)) % 360] + nshift = 360.0 / nloopstot + angles = [ + (ashift + nshift * nloops[src]) % 360, + (ashift + nshift * (nloops[src] + 1)) % 360, + ] nloops[src] += 1 else: angles.append(angles[0] + 360) @@ -1123,11 +1160,13 @@ def callback_edge_offset(event): if diffi > diff: idiff = i diff = diffi - angles = angles[idiff: idiff + 2] + angles = angles[idiff : idiff + 2] ashift = angles[0] nshift = (angles[1] - angles[0]) / nloopstot - angles = [(ashift + nshift * nloops[src]), - (ashift + nshift * (nloops[src] + 1))] + angles = [ + (ashift + nshift * nloops[src]), + (ashift + nshift * (nloops[src] + 1)), + ] nloops[src] += 1 # this is not great, but alright @@ -1138,32 +1177,40 @@ def callback_edge_offset(event): else: angmid1 = angles[0] + 0.5 * (angspan - 180) + 45 angmid2 = angles[1] - 0.5 * (angspan - 180) - 45 - aux1 = (x1 + 0.2 * dx * cos(pi / 180 * angmid1), - y1 + 0.2 * dy * sin(pi / 180 * angmid1)) - aux2 = (x1 + 0.2 * dx * cos(pi / 180 * angmid2), - y1 + 0.2 * dy * sin(pi / 180 * angmid2)) + aux1 = ( + x1 + 0.2 * dx * cos(pi / 180 * angmid1), + y1 + 0.2 * dy * sin(pi / 180 * angmid1), + ) + aux2 = ( + x1 + 0.2 * dx * cos(pi / 180 * angmid2), + y1 + 0.2 * dy * sin(pi / 180 * angmid2), + ) start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) path = Path( [start, aux1, aux2, end], # Cubic bezier by mpl - codes=[1, 4, 4, 4]) + codes=[1, 4, 4, 4], + ) else: - curved = edge['curved'] if has_curved else False + curved = edge["curved"] if has_curved else False if curved: - aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), \ - (2 * y1 + y2) / 3.0 + edge.curved * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), \ - (y1 + 2 * y2) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) path = Path( [start, aux1, aux2, end], # Cubic bezier by mpl - codes=[1, 4, 4, 4]) + codes=[1, 4, 4, 4], + ) else: start = shrink_vertex(ax, (x2, y2), (x1, y1), vsizes[src]) end = shrink_vertex(ax, (x1, y1), (x2, y2), vsizes[tgt]) @@ -1176,7 +1223,8 @@ def callback_edge_offset(event): lw=edge_width, color=ec, alpha=ealpha, - zorder=ezorder[ie]) + zorder=ezorder[ie], + ) ax.add_artist(arrow) # Store arrows and their sources and targets for autoscaling @@ -1184,6 +1232,6 @@ def callback_edge_offset(event): # Autoscaling during zoom, figure resize, reset axis limits callback = callback_factory(ax, vcoord, vsizes, arrows) - ax.get_figure().canvas.mpl_connect('resize_event', callback) - ax.callbacks.connect('xlim_changed', callback) - ax.callbacks.connect('ylim_changed', callback) + ax.get_figure().canvas.mpl_connect("resize_event", callback) + ax.callbacks.connect("xlim_changed", callback) + ax.callbacks.connect("ylim_changed", callback) diff --git a/src/igraph/drawing/metamagic.py b/src/igraph/drawing/metamagic.py index 44a22263e..3e78a39da 100644 --- a/src/igraph/drawing/metamagic.py +++ b/src/igraph/drawing/metamagic.py @@ -60,8 +60,8 @@ class VisualEdgeBuilder(AttributeCollectorBase): @see: AttributeCollectorMeta, AttributeCollectorBase """ -from ConfigParser import NoOptionError -from itertools import izip +from configparser import NoOptionError + from igraph.configuration import Configuration @@ -72,21 +72,21 @@ class VisualEdgeBuilder(AttributeCollectorBase): class AttributeSpecification(object): """Class that describes how the value of a given attribute should be retrieved. - + The class contains the following members: - + - C{name}: the name of the attribute. This is also used when we are trying to get its value from a vertex/edge attribute of a graph. - + - C{alt_name}: alternative name of the attribute. This is used when we are trying to get its value from a Python dict or an L{igraph.Configuration} object. If omitted at construction time, it will be equal to C{name}. - + - C{default}: the default value of the attribute when none of the sources we try can provide a meaningful value. - + - C{transform}: optional transformation to be performed on the attribute value. If C{None} or omitted, it defaults to the type of the default value. @@ -95,11 +95,9 @@ class AttributeSpecification(object): index in order to derive the value of the attribute. """ - __slots__ = ("name", "alt_name", "default", "transform", "accessor", - "func") + __slots__ = ("name", "alt_name", "default", "transform", "accessor", "func") - def __init__(self, name, default=None, alt_name=None, transform=None, - func=None): + def __init__(self, name, default=None, alt_name=None, transform=None, func=None): if isinstance(default, tuple): default, transform = default @@ -111,7 +109,7 @@ def __init__(self, name, default=None, alt_name=None, transform=None, self.accessor = None if self.transform and not hasattr(self.transform, "__call__"): - raise TypeError, "transform must be callable" + raise TypeError("transform must be callable") if self.transform is None and self.default is not None: self.transform = type(self.default) @@ -119,7 +117,7 @@ def __init__(self, name, default=None, alt_name=None, transform=None, class AttributeCollectorMeta(type): """Metaclass for attribute collector classes - + Classes that use this metaclass are intended to collect vertex/edge attributes from various sources (a Python dict, a vertex/edge sequence, default values from the igraph configuration and such) in a given @@ -154,7 +152,7 @@ class AttributeCollectorMeta(type): def __new__(mcs, name, bases, attrs): attr_specs = [] - for attr, value in attrs.iteritems(): + for attr, value in attrs.items(): if attr.startswith("_") or hasattr(value, "__call__"): continue if isinstance(value, AttributeSpecification): @@ -173,37 +171,37 @@ def __new__(mcs, name, bases, attrs): attrs["_attributes"] = attr_specs attrs["Element"] = mcs.record_generator( - "%s.Element" % name, - (attr_spec.name for attr_spec in attr_specs) + "%s.Element" % name, (attr_spec.name for attr_spec in attr_specs) ) - return super(AttributeCollectorMeta, mcs).__new__(mcs, \ - name, bases, attrs) + return super(AttributeCollectorMeta, mcs).__new__(mcs, name, bases, attrs) @classmethod def record_generator(mcs, name, slots): """Generates a simple class that has the given slots and nothing else""" + class Element(object): """A simple class that holds the attributes collected by the attribute collector""" + __slots__ = tuple(slots) + def __init__(self, attrs=()): for attr, value in attrs: setattr(self, attr, value) + Element.__name__ = name return Element -class AttributeCollectorBase(object): +class AttributeCollectorBase(object, metaclass=AttributeCollectorMeta): """Base class for attribute collector subclasses. Classes that inherit this class may use a declarative syntax to specify which vertex or edge attributes they intend to collect. See L{AttributeCollectorMeta} for the details. """ - __metaclass__ = AttributeCollectorMeta - - def __init__(self, seq, kwds = None): + def __init__(self, seq, kwds=None): """Constructs a new attribute collector that uses the given vertex/edge sequence and the given dict as data sources. @@ -213,15 +211,15 @@ def __init__(self, seq, kwds = None): attributes collected from I{seq} if necessary. """ elt = self.__class__.Element - self._cache = [elt() for _ in xrange(len(seq))] + self._cache = [elt() for _ in range(len(seq))] self.seq = seq self.kwds = kwds or {} for attr_spec in self._attributes: - values = self._collect_attributes(attr_spec) + values = self._collect_attributes(attr_spec) attr_name = attr_spec.name - for cache_elt, val in izip(self._cache, values): + for cache_elt, val in zip(self._cache, values): setattr(cache_elt, attr_name, val) def _collect_attributes(self, attr_spec, config=None): @@ -260,7 +258,7 @@ def _collect_attributes(self, attr_spec, config=None): n = len(seq) - # Special case if the attribute name is "label" + # Special case if the attribute name is "label" if attr_spec.name == "label": if attr_spec.alt_name in kwds and kwds[attr_spec.alt_name] is None: return [None] * n @@ -269,7 +267,7 @@ def _collect_attributes(self, attr_spec, config=None): # values, call it and store the results if attr_spec.func is not None: func = attr_spec.func - result = [func(i) for i in xrange(n)] + result = [func(i) for i in range(n)] return result # Get the configuration object @@ -294,8 +292,7 @@ def _collect_attributes(self, attr_spec, config=None): len(result) except TypeError: result = [result] * n - result = [result[idx] or attrs[idx] \ - for idx in xrange(len(result))] + result = [result[idx] or attrs[idx] for idx in range(len(result))] # Special case for string overrides, strings are not treated # as sequences here @@ -314,10 +311,10 @@ def _collect_attributes(self, attr_spec, config=None): # Ensure that the length is n while len(result) < n: - if len(result) <= n/2: + if len(result) <= n / 2: result.extend(result) else: - result.extend(result[0:(n-len(result))]) + result.extend(result[0 : (n - len(result))]) # By now, the length of the result vector should be n as requested # Get the configuration defaults @@ -330,7 +327,7 @@ def _collect_attributes(self, attr_spec, config=None): default = attr_spec.default # Fill the None values with the default values - for idx in xrange(len(result)): + for idx in range(len(result)): if result[idx] is None: result[idx] = default @@ -341,7 +338,6 @@ def _collect_attributes(self, attr_spec, config=None): return result - def __getitem__(self, index): """Returns the collected attributes of the vertex/edge with the given index.""" @@ -351,6 +347,3 @@ def __getitem__(self, index): def __len__(self): return len(self.seq) - - - diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index 741aa9ef6..2a8882189 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -16,11 +16,10 @@ name in the C{shape} attribute of vertices. """ -from __future__ import division __all__ = ["ShapeDrawerDirectory"] -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -47,9 +46,10 @@ from igraph.drawing.utils import Point from igraph.utils import consecutive_pairs + class ShapeDrawer(object): """Static class, the ancestor of all vertex shape drawer classes. - + Custom shapes must implement at least the C{draw_path} method of the class. The method I{must not} stroke or fill, it should just set up the current Cairo path appropriately.""" @@ -72,8 +72,7 @@ def draw_path(ctx, center_x, center_y, width, height=None): # pylint: disable-msg=W0613 @staticmethod - def intersection_point(center_x, center_y, source_x, source_y, \ - width, height=None): + def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the shape centered at (center_x, center_y) intersects with a line drawn from (source_x, source_y) to (center_x, center_y). @@ -94,6 +93,7 @@ class NullDrawer(ShapeDrawer): """Static drawer class which draws nothing. This class is used for graph vertices with unknown shapes""" + names = ["null", "none", "empty", "hidden", ""] @staticmethod @@ -104,6 +104,7 @@ def draw_path(ctx, center_x, center_y, width, height=None): class RectangleDrawer(ShapeDrawer): """Static class which draws rectangular vertices""" + names = "rectangle rect rectangular square box" @staticmethod @@ -112,62 +113,61 @@ def draw_path(ctx, center_x, center_y, width, height=None): or filling it. @see: ShapeDrawer.draw_path""" height = height or width - ctx.rectangle(center_x - width/2, center_y - height/2, - width, height) + ctx.rectangle(center_x - width / 2, center_y - height / 2, width, height) # pylint: disable-msg=C0103, R0911 # R0911: too many return statements @staticmethod - def intersection_point(center_x, center_y, source_x, source_y, \ - width, height=None): + def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the rectangle centered at (center_x, center_y) having the given width and height intersects with a line drawn from (source_x, source_y) to (center_x, center_y). @see: ShapeDrawer.intersection_point""" height = height or width - delta_x, delta_y = center_x-source_x, center_y-source_y + delta_x, delta_y = center_x - source_x, center_y - source_y if delta_x == 0 and delta_y == 0: return center_x, center_y if delta_y > 0 and delta_x <= delta_y and delta_x >= -delta_y: # this is the top edge - ry = center_y - height/2 - ratio = (height/2) / delta_y - return center_x-ratio*delta_x, ry + ry = center_y - height / 2 + ratio = (height / 2) / delta_y + return center_x - ratio * delta_x, ry if delta_y < 0 and delta_x <= -delta_y and delta_x >= delta_y: # this is the bottom edge - ry = center_y + height/2 - ratio = (height/2) / -delta_y - return center_x-ratio*delta_x, ry + ry = center_y + height / 2 + ratio = (height / 2) / -delta_y + return center_x - ratio * delta_x, ry if delta_x > 0 and delta_y <= delta_x and delta_y >= -delta_x: # this is the left edge - rx = center_x - width/2 - ratio = (width/2) / delta_x - return rx, center_y-ratio*delta_y + rx = center_x - width / 2 + ratio = (width / 2) / delta_x + return rx, center_y - ratio * delta_y if delta_x < 0 and delta_y <= -delta_x and delta_y >= delta_x: # this is the right edge - rx = center_x + width/2 - ratio = (width/2) / -delta_x - return rx, center_y-ratio*delta_y + rx = center_x + width / 2 + ratio = (width / 2) / -delta_x + return rx, center_y - ratio * delta_y if delta_x == 0: if delta_y > 0: - return center_x, center_y - height/2 - return center_x, center_y + height/2 + return center_x, center_y - height / 2 + return center_x, center_y + height / 2 if delta_y == 0: if delta_x > 0: - return center_x - width/2, center_y - return center_x + width/2, center_y + return center_x - width / 2, center_y + return center_x + width / 2, center_y class CircleDrawer(ShapeDrawer): """Static class which draws circular vertices""" + names = "circle circular" @staticmethod @@ -178,41 +178,39 @@ def draw_path(ctx, center_x, center_y, width, height=None): Height is ignored, it is the width that determines the diameter of the circle. @see: ShapeDrawer.draw_path""" - ctx.arc(center_x, center_y, width/2, 0, 2*pi) + ctx.arc(center_x, center_y, width / 2, 0, 2 * pi) @staticmethod - def intersection_point(center_x, center_y, source_x, source_y, \ - width, height=None): + def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the circle centered at (center_x, center_y) intersects with a line drawn from (source_x, source_y) to (center_x, center_y). @see: ShapeDrawer.intersection_point""" height = height or width - angle = atan2(center_y-source_y, center_x-source_x) - return center_x-width/2 * cos(angle), \ - center_y-height/2* sin(angle) + angle = atan2(center_y - source_y, center_x - source_x) + return center_x - width / 2 * cos(angle), center_y - height / 2 * sin(angle) class UpTriangleDrawer(ShapeDrawer): """Static class which draws upright triangles""" + names = "triangle triangle-up up-triangle arrow arrow-up up-arrow" @staticmethod def draw_path(ctx, center_x, center_y, width, height=None): """Draws an upright triangle on the Cairo context without stroking or filling it. - + @see: ShapeDrawer.draw_path""" height = height or width - ctx.move_to(center_x-width/2, center_y+height/2) - ctx.line_to(center_x, center_y-height/2) - ctx.line_to(center_x+width/2, center_y+height/2) + ctx.move_to(center_x - width / 2, center_y + height / 2) + ctx.line_to(center_x, center_y - height / 2) + ctx.line_to(center_x + width / 2, center_y + height / 2) ctx.close_path() @staticmethod - def intersection_point(center_x, center_y, source_x, source_y, \ - width, height=None): + def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the triangle centered at (center_x, center_y) intersects with a line drawn from (source_x, source_y) to (center_x, center_y). @@ -222,25 +220,26 @@ def intersection_point(center_x, center_y, source_x, source_y, \ height = height or width return center_x, center_y + class DownTriangleDrawer(ShapeDrawer): """Static class which draws triangles pointing down""" + names = "down-triangle triangle-down arrow-down down-arrow" @staticmethod def draw_path(ctx, center_x, center_y, width, height=None): """Draws a triangle on the Cairo context without stroking or filling it. - + @see: ShapeDrawer.draw_path""" height = height or width - ctx.move_to(center_x-width/2, center_y-height/2) - ctx.line_to(center_x, center_y+height/2) - ctx.line_to(center_x+width/2, center_y-height/2) + ctx.move_to(center_x - width / 2, center_y - height / 2) + ctx.line_to(center_x, center_y + height / 2) + ctx.line_to(center_x + width / 2, center_y - height / 2) ctx.close_path() @staticmethod - def intersection_point(center_x, center_y, source_x, source_y, \ - width, height=None): + def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the triangle centered at (center_x, center_y) intersects with a line drawn from (source_x, source_y) to (center_x, center_y). @@ -250,26 +249,27 @@ def intersection_point(center_x, center_y, source_x, source_y, \ height = height or width return center_x, center_y + class DiamondDrawer(ShapeDrawer): """Static class which draws diamonds (i.e. rhombuses)""" + names = "diamond rhombus" @staticmethod def draw_path(ctx, center_x, center_y, width, height=None): """Draws a rhombus on the Cairo context without stroking or filling it. - + @see: ShapeDrawer.draw_path""" height = height or width - ctx.move_to(center_x-width/2, center_y) - ctx.line_to(center_x, center_y+height/2) - ctx.line_to(center_x+width/2, center_y) - ctx.line_to(center_x, center_y-height/2) + ctx.move_to(center_x - width / 2, center_y) + ctx.line_to(center_x, center_y + height / 2) + ctx.line_to(center_x + width / 2, center_y) + ctx.line_to(center_x, center_y - height / 2) ctx.close_path() @staticmethod - def intersection_point(center_x, center_y, source_x, source_y, \ - width, height=None): + def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the rhombus centered at (center_x, center_y) intersects with a line drawn from (source_x, source_y) to (center_x, center_y). @@ -293,20 +293,22 @@ def intersection_point(center_x, center_y, source_x, source_y, \ height = copysign(height, delta_y) f = height / (height + width * delta_y / delta_x) - return center_x + f * width / 2, center_y + (1-f) * height / 2 + return center_x + f * width / 2, center_y + (1 - f) * height / 2 + ##################################################################### + class PolygonDrawer(AbstractCairoDrawer): """Class that is used to draw polygons. - + The corner points of the polygon can be set by the C{points} property of the drawer, or passed at construction time. Most drawing methods in this class also have an extra C{points} argument that can be used to override the set of points in the C{points} property.""" - def __init__(self, context, bbox=(1, 1), points = []): + def __init__(self, context, bbox=(1, 1), points=[]): """Constructs a new polygon drawer that draws on the given Cairo context. @@ -354,13 +356,12 @@ def draw_path(self, points=None, corner_radius=0): # is the smaller of the radii on the two sides adjacent to # the corner. points = [Point(*point) for point in points] - side_vecs = [v-u for u, v in consecutive_pairs(points, circular=True)] + side_vecs = [v - u for u, v in consecutive_pairs(points, circular=True)] half_side_lengths = [side.length() / 2 for side in side_vecs] corner_radii = [corner_radius] * len(points) - for idx in xrange(len(corner_radii)): + for idx in range(len(corner_radii)): prev_idx = -1 if idx == 0 else idx - 1 - radii = [corner_radius, half_side_lengths[prev_idx], - half_side_lengths[idx]] + radii = [corner_radius, half_side_lengths[prev_idx], half_side_lengths[idx]] corner_radii[idx] = min(radii) # Okay, move to the last corner, adjusted by corner_radii[-1] @@ -375,8 +376,9 @@ def draw_path(self, points=None, corner_radius=0): ctx.line_to(*v.towards(u, radius)) aux1 = v.towards(u, radius / 2) aux2 = v.towards(w, radius / 2) - ctx.curve_to(aux1.x, aux1.y, aux2.x, aux2.y, - *v.towards(w, corner_radii[idx])) + ctx.curve_to( + aux1.x, aux1.y, aux2.x, aux2.y, *v.towards(w, corner_radii[idx]) + ) u = v def draw(self, points=None): @@ -389,12 +391,14 @@ def draw(self, points=None): self.draw_path(points) self.context.stroke() + ##################################################################### + class ShapeDrawerDirectory(object): """Static class that resolves shape names to their corresponding shape drawer classes. - + Classes that are derived from L{ShapeDrawer} in this module are automatically registered by L{ShapeDrawerDirectory} when the module is loaded for the first time. @@ -409,7 +413,7 @@ def register(cls, drawer_class): @param drawer_class: the shape drawer class to be registered """ names = drawer_class.names - if isinstance(names, (str, unicode)): + if isinstance(names, str): names = names.split() for name in names: @@ -420,7 +424,7 @@ def register_namespace(cls, namespace): """Registers all L{ShapeDrawer} classes in the given namespace @param namespace: a Python dict mapping names to Python objects.""" - for name, value in namespace.iteritems(): + for name, value in namespace.items(): if name.startswith("__"): continue if isinstance(value, type): @@ -430,7 +434,7 @@ def register_namespace(cls, namespace): @classmethod def resolve(cls, shape): """Given a shape name, returns the corresponding shape drawer class - + @param shape: the name of the shape @return: the corresponding shape drawer class @@ -445,7 +449,7 @@ def resolve(cls, shape): def resolve_default(cls, shape, default=NullDrawer): """Given a shape name, returns the corresponding shape drawer class or the given default shape drawer if the shape name is unknown. - + @param shape: the name of the shape @param default: the default shape drawer to return when the shape is unknown @@ -454,5 +458,5 @@ def resolve_default(cls, shape, default=NullDrawer): """ return cls.known_shapes.get(shape, default) -ShapeDrawerDirectory.register_namespace(sys.modules[__name__].__dict__) +ShapeDrawerDirectory.register_namespace(sys.modules[__name__].__dict__) diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index bff01c897..323e8ec20 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -6,7 +6,6 @@ import re -from igraph.compat import property from igraph.drawing.baseclasses import AbstractCairoDrawer from warnings import warn @@ -17,14 +16,17 @@ ##################################################################### + class TextAlignment(object): """Text alignment constants.""" LEFT, CENTER, RIGHT = "left", "center", "right" TOP, BOTTOM = "top", "bottom" + ##################################################################### + class TextDrawer(AbstractCairoDrawer): """Class that draws text on a Cairo context. @@ -70,7 +72,7 @@ def draw(self, wrap=False): dy = bbox.height - total_height - yb + font_descent elif self.valign == self.CENTER: # Centered vertical alignment - dy = (bbox.height - total_height - yb + font_descent + line_height) / 2. + dy = (bbox.height - total_height - yb + font_descent + line_height) / 2.0 else: # Top vertical alignment dy = line_height @@ -80,7 +82,7 @@ def draw(self, wrap=False): ctx.show_text(line) ctx.new_path() - def get_text_layout(self, x = None, y = None, width = None, wrap = False): + def get_text_layout(self, x=None, y=None, width=None, wrap=False): """Calculates the layout of the current text. `x` and `y` denote the coordinates where the drawing should start. If they are both ``None``, the current position of the context will be used. @@ -131,7 +133,7 @@ def get_text_layout(self, x = None, y = None, width = None, wrap = False): if width is None: width = self.text_extents()[2] for line, line_width, x_bearing in iterlines: - result.append((x + (width-line_width)/2. - x_bearing, y, line)) + result.append((x + (width - line_width) / 2.0 - x_bearing, y, line)) y += line_height elif self.halign == self.RIGHT: @@ -146,12 +148,12 @@ def get_text_layout(self, x = None, y = None, width = None, wrap = False): else: # Left alignment for line, _, x_bearing in iterlines: - result.append((x-x_bearing, y, line)) + result.append((x - x_bearing, y, line)) y += line_height return result - def draw_at(self, x = None, y = None, width = None, wrap = False): + def draw_at(self, x=None, y=None, width=None, wrap=False): """Draws the text by setting up an appropriate path on the Cairo context and filling it. `x` and `y` denote the coordinates where the drawing should start. If they are both ``None``, the current position @@ -263,8 +265,14 @@ def text_extents(self): if len(lines) <= 1: return self.context.text_extents(self.text) - x_bearing, y_bearing, width, height, x_advance, y_advance = \ - self.context.text_extents(lines[0]) + ( + x_bearing, + y_bearing, + width, + height, + x_advance, + y_advance, + ) = self.context.text_extents(lines[0]) line_height = self.context.font_extents()[2] for line in lines[1:]: @@ -275,10 +283,12 @@ def text_extents(self): return x_bearing, y_bearing, width, height, x_advance, y_advance + def test(): """Testing routine for L{TextDrawer}""" import math from igraph.drawing.utils import find_cairo + cairo = find_cairo() text = "The quick brown fox\njumps over a\nlazy dog" @@ -289,7 +299,7 @@ def test(): drawer = TextDrawer(context, text) context.set_source_rgb(1, 1, 1) - context.set_font_size(16.) + context.set_font_size(16.0) context.rectangle(0, 0, width, height) context.fill() @@ -344,15 +354,17 @@ def mark_point(red, green, blue): context.set_source_rgb(0, 0, 0) drawer.halign = halign drawer.valign = valign - drawer.bbox = (i*200, j*200+200, i*200+200, j*200+400) + drawer.bbox = (i * 200, j * 200 + 200, i * 200 + 200, j * 200 + 400) drawer.draw() # Mark the new reference point mark_point(1, 0, 0) # Testing TextDrawer.wrap() - drawer.text = "Jackdaws love my big sphinx of quartz. Yay, wrapping! " + \ - "Jackdaws love my big sphinx of quartz.\n\n" + \ - "Jackdaws love my big sphinx of quartz." + drawer.text = ( + "Jackdaws love my big sphinx of quartz. Yay, wrapping! " + + "Jackdaws love my big sphinx of quartz.\n\n" + + "Jackdaws love my big sphinx of quartz." + ) drawer.valign = TextDrawer.TOP for i, halign in enumerate(("left", "center", "right")): context.move_to(i * 200, 840) @@ -370,6 +382,6 @@ def mark_point(red, green, blue): surface.write_to_png("test.png") + if __name__ == "__main__": test() - diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 205b55044..32ba062f8 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -2,8 +2,7 @@ Utility classes for drawing routines. """ -from igraph.compat import property -from itertools import izip + from math import atan2, cos, sin from operator import itemgetter @@ -12,6 +11,7 @@ ##################################################################### + class Rectangle(object): """Class representing a rectangle.""" @@ -174,13 +174,13 @@ def contract(self, margins): margins = [float(margins)] * 4 if len(margins) != 4: raise ValueError("margins must be a 4-tuple or a single number") - nx1, ny1 = self._left+margins[0], self._top+margins[1] - nx2, ny2 = self._right-margins[2], self._bottom-margins[3] + nx1, ny1 = self._left + margins[0], self._top + margins[1] + nx2, ny2 = self._right - margins[2], self._bottom - margins[3] if nx1 > nx2: - nx1 = (nx1+nx2)/2. + nx1 = (nx1 + nx2) / 2.0 nx2 = nx1 if ny1 > ny2: - ny1 = (ny1+ny2)/2. + ny1 = (ny1 + ny2) / 2.0 ny2 = ny1 return self.__class__(nx1, ny1, nx2, ny2) @@ -210,8 +210,12 @@ def isdisjoint(self, other): >>> r3.isdisjoint(r1) True """ - return self._left > other._right or self._right < other._left \ - or self._top > other._bottom or self._bottom < other._top + return ( + self._left > other._right + or self._right < other._left + or self._top > other._bottom + or self._bottom < other._top + ) def isempty(self): """Returns ``True`` if the rectangle is empty (i.e. it has zero @@ -249,10 +253,13 @@ def intersection(self, other): """ if self.isdisjoint(other): return Rectangle(0, 0, 0, 0) - return Rectangle(max(self._left, other._left), - max(self._top, other._top), - min(self._right, other._right), - min(self._bottom, other._bottom)) + return Rectangle( + max(self._left, other._left), + max(self._top, other._top), + min(self._right, other._right), + min(self._bottom, other._bottom), + ) + __and__ = intersection def translate(self, dx, dy): @@ -293,10 +300,13 @@ def union(self, other): >>> r1.union(r3) Rectangle(10.0, 10.0, 90.0, 90.0) """ - return Rectangle(min(self._left, other._left), - min(self._top, other._top), - max(self._right, other._right), - max(self._bottom, other._bottom)) + return Rectangle( + min(self._left, other._left), + min(self._top, other._top), + max(self._right, other._right), + max(self._bottom, other._bottom), + ) + __or__ = union def __ior__(self, other): @@ -315,15 +325,20 @@ def __ior__(self, other): >>> r1 Rectangle(10.0, 10.0, 90.0, 90.0) """ - self._left = min(self._left, other._left) - self._top = min(self._top, other._top) - self._right = max(self._right, other._right) + self._left = min(self._left, other._left) + self._top = min(self._top, other._top) + self._right = max(self._right, other._right) self._bottom = max(self._bottom, other._bottom) return self def __repr__(self): - return "%s(%s, %s, %s, %s)" % (self.__class__.__name__, \ - self._left, self._top, self._right, self._bottom) + return "%s(%s, %s, %s, %s)" % ( + self.__class__.__name__, + self._left, + self._top, + self._right, + self._bottom, + ) def __eq__(self, other): return self.coords == other.coords @@ -334,14 +349,13 @@ def __ne__(self, other): def __bool__(self): return self._left != self._right or self._top != self._bottom - def __nonzero__(self): - return self._left != self._right or self._top != self._bottom - def __hash__(self): return hash(self.coords) + ##################################################################### + class BoundingBox(Rectangle): """Class representing a bounding box (a rectangular area) that encloses some objects.""" @@ -358,9 +372,9 @@ def __ior__(self, other): >>> print(box1) BoundingBox(10.0, 20.0, 100.0, 90.0) """ - self._left = min(self._left, other._left) - self._top = min(self._top, other._top) - self._right = max(self._right, other._right) + self._left = min(self._left, other._left) + self._top = min(self._top, other._top) + self._right = max(self._right, other._right) self._bottom = max(self._bottom, other._bottom) return self @@ -378,10 +392,10 @@ def __or__(self, other): BoundingBox(10.0, 20.0, 100.0, 90.0) """ return self.__class__( - min(self._left, other._left), - min(self._top, other._top), - max(self._right, other._right), - max(self._bottom, other._bottom) + min(self._left, other._left), + min(self._top, other._top), + max(self._right, other._right), + max(self._bottom, other._bottom), ) @@ -394,13 +408,17 @@ class FakeModule(object): def __getattr__(self, _): raise AttributeError("plotting not available") + def __call__(self, _): raise TypeError("plotting not available") + def __setattr__(self, key, value): raise AttributeError("plotting not available") + ##################################################################### + def find_cairo(): """Tries to import the ``cairo`` Python module if it is installed, also trying ``cairocffi`` (a drop-in replacement of ``cairo``). @@ -416,8 +434,10 @@ def find_cairo(): pass return module + ##################################################################### + def find_matplotlib(): """Tries to import the ``cairo`` Python module if it is installed, also trying ``cairocffi`` (a drop-in replacement of ``cairo``). @@ -425,6 +445,7 @@ def find_matplotlib(): """ try: import matplotlib as mpl + has_mpl = True except ImportError: mpl = FakeModule() @@ -437,12 +458,15 @@ def find_matplotlib(): return mpl, plt + ##################################################################### + class Point(tuple): """Class representing a point on the 2D plane.""" + __slots__ = () - _fields = ('x', 'y') + _fields = ("x", "y") def __new__(cls, x, y): """Creates a new point with the given coordinates""" @@ -451,16 +475,16 @@ def __new__(cls, x, y): # pylint: disable-msg=W0622 # W0622: redefining built-in 'len' @classmethod - def _make(cls, iterable, new = tuple.__new__, len = len): + def _make(cls, iterable, new=tuple.__new__, len=len): """Creates a new point from a sequence or iterable""" result = new(cls, iterable) if len(result) != 2: - raise TypeError('Expected 2 arguments, got %d' % len(result)) + raise TypeError("Expected 2 arguments, got %d" % len(result)) return result def __repr__(self): """Returns a nicely formatted representation of the point""" - return 'Point(x=%r, y=%r)' % self + return "Point(x=%r, y=%r)" % self def _asdict(self): """Returns a new dict which maps field names to their values""" @@ -471,9 +495,9 @@ def _asdict(self): def _replace(self, **kwds): """Returns a new point object replacing specified fields with new values""" - result = self._make(map(kwds.pop, ('x', 'y'), self)) + result = self._make(map(kwds.pop, ("x", "y"), self)) if kwds: - raise ValueError('Got unexpected field names: %r' % kwds.keys()) + raise ValueError("Got unexpected field names: %r" % list(kwds.keys())) return result def __getnewargs__(self): @@ -485,20 +509,21 @@ def __getnewargs__(self): def __add__(self, other): """Adds the coordinates of a point to another one""" - return self.__class__(x = self.x + other.x, y = self.y + other.y) + return self.__class__(x=self.x + other.x, y=self.y + other.y) def __sub__(self, other): """Subtracts the coordinates of a point to another one""" - return self.__class__(x = self.x - other.x, y = self.y - other.y) + return self.__class__(x=self.x - other.x, y=self.y - other.y) def __mul__(self, scalar): """Multiplies the coordinates by a scalar""" - return self.__class__(x = self.x * scalar, y = self.y * scalar) + return self.__class__(x=self.x * scalar, y=self.y * scalar) + __rmul__ = __mul__ def __div__(self, scalar): """Divides the coordinates by a scalar""" - return self.__class__(x = self.x / scalar, y = self.y / scalar) + return self.__class__(x=self.x / scalar, y=self.y / scalar) def as_polar(self): """Returns the polar coordinate representation of the point. @@ -520,7 +545,7 @@ def distance(self, other): dx, dy = self.x - other.x, self.y - other.y return (dx * dx + dy * dy) ** 0.5 - def interpolate(self, other, ratio = 0.5): + def interpolate(self, other, ratio=0.5): """Linearly interpolates between the coordinates of this point and another one. @@ -529,8 +554,10 @@ def interpolate(self, other, ratio = 0.5): return this point, 1 will return the other point. """ ratio = float(ratio) - return Point(x = self.x * (1.0 - ratio) + other.x * ratio, \ - y = self.y * (1.0 - ratio) + other.y * ratio) + return Point( + x=self.x * (1.0 - ratio) + other.x * ratio, + y=self.y * (1.0 - ratio) + other.y * ratio, + ) def length(self): """Returns the length of the vector pointing from the origin to this @@ -542,23 +569,22 @@ def normalized(self): after normalization. Returns the normalized point.""" len = self.length() if len == 0: - return Point(x = self.x, y = self.y) - return Point(x = self.x / len, y = self.y / len) + return Point(x=self.x, y=self.y) + return Point(x=self.x / len, y=self.y / len) def sq_length(self): """Returns the squared length of the vector pointing from the origin to this point.""" - return (self.x ** 2 + self.y ** 2) + return self.x ** 2 + self.y ** 2 - def towards(self, other, distance = 0): + def towards(self, other, distance=0): """Returns the point that is at a given distance from this point towards another one.""" if not distance: return self angle = atan2(other.y - self.y, other.x - self.x) - return Point(self.x + distance * cos(angle), - self.y + distance * sin(angle)) + return Point(self.x + distance * cos(angle), self.y + distance * sin(angle)) @classmethod def FromPolar(cls, radius, angle): @@ -569,4 +595,3 @@ def FromPolar(cls, radius, angle): the origin. """ return cls(radius * cos(angle), radius * sin(angle)) - diff --git a/src/igraph/drawing/vertex.py b/src/igraph/drawing/vertex.py index a84ba53e4..bc6d6b2a8 100644 --- a/src/igraph/drawing/vertex.py +++ b/src/igraph/drawing/vertex.py @@ -10,10 +10,10 @@ from igraph.drawing.shapes import ShapeDrawerDirectory from math import pi -__all__ = ["AbstractVertexDrawer", "AbstractCairoVertexDrawer", \ - "DefaultVertexDrawer"] +__all__ = ["AbstractVertexDrawer", "AbstractCairoVertexDrawer", "DefaultVertexDrawer"] __license__ = "GPL" + class AbstractVertexDrawer(AbstractDrawer): """Abstract vertex drawer object from which all concrete vertex drawer implementations are derived.""" @@ -31,7 +31,7 @@ def __init__(self, palette, layout): def draw(self, visual_vertex, vertex, coords): """Draws the given vertex. - + @param visual_vertex: object specifying the visual properties of the vertex. Its structure is defined by the VisualVertexBuilder of the L{DefaultGraphDrawer}; see its source code. @@ -41,6 +41,7 @@ def draw(self, visual_vertex, vertex, coords): """ raise NotImplementedError("abstract class") + class AbstractCairoVertexDrawer(AbstractVertexDrawer, AbstractCairoDrawer): """Abstract base class for vertex drawers that draw on a Cairo canvas.""" @@ -60,6 +61,7 @@ def __init__(self, context, bbox, palette, layout): AbstractCairoDrawer.__init__(self, context, bbox) AbstractVertexDrawer.__init__(self, palette, layout) + class DefaultVertexDrawer(AbstractCairoVertexDrawer): """The default vertex drawer implementation of igraph.""" @@ -70,28 +72,38 @@ def __init__(self, context, bbox, palette, layout): def _construct_visual_vertex_builder(self): class VisualVertexBuilder(AttributeCollectorBase): """Collects some visual properties of a vertex for drawing""" + _kwds_prefix = "vertex_" color = ("red", self.palette.get) frame_color = ("black", self.palette.get) frame_width = 1.0 label = None - label_angle = -pi/2 - label_dist = 0.0 + label_angle = -pi / 2 + label_dist = 0.0 label_color = ("black", self.palette.get) - font = 'sans-serif' - label_size = 14.0 + font = "sans-serif" + label_size = 14.0 position = dict(func=self.layout.__getitem__) shape = ("circle", ShapeDrawerDirectory.resolve_default) size = 20.0 width = None height = None + return VisualVertexBuilder def draw(self, visual_vertex, vertex, coords): context = self.context - width = visual_vertex.width if visual_vertex.width is not None else visual_vertex.size - height = visual_vertex.height if visual_vertex.height is not None else visual_vertex.size + width = ( + visual_vertex.width + if visual_vertex.width is not None + else visual_vertex.size + ) + height = ( + visual_vertex.height + if visual_vertex.height is not None + else visual_vertex.size + ) visual_vertex.shape.draw_path(context, coords[0], coords[1], width, height) context.set_source_rgba(*visual_vertex.color) @@ -99,5 +111,3 @@ def draw(self, visual_vertex, vertex, coords): context.set_source_rgba(*visual_vertex.frame_color) context.set_line_width(visual_vertex.frame_width) context.stroke() - - diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 540febca7..78d826f1c 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -8,7 +8,7 @@ `igraph.Graph.Formula()`. """ -from cStringIO import StringIO +from io import StringIO from igraph.datatypes import UniqueIdGenerator import re @@ -17,7 +17,7 @@ __all__ = ["construct_graph_from_formula"] -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -37,13 +37,14 @@ 02110-1301 USA """ + def generate_edges(formula): """Parses an edge specification from the head of the given formula part and yields the following: - + - startpoint(s) of the edge by vertex names - endpoint(s) of the edge by names or an empty list if the vertices are isolated - - a pair of bools to denote whether we had arrowheads at the start and end vertices + - a pair of bools to denote whether we had arrowheads at the start and end vertices """ if formula == "": yield [], [""], [False, False] @@ -55,7 +56,7 @@ def generate_edges(formula): parsing_vertices = True # Tokenize the formula - token_gen = tokenize.generate_tokens(StringIO(formula).next) + token_gen = tokenize.generate_tokens(StringIO(formula).__next__) for token_info in token_gen: # Do the state transitions token_type, tok, _, _, _ = token_info @@ -90,7 +91,10 @@ def generate_edges(formula): # End markers are fine pass else: - msg = "invalid token found in edge specification: %s; token_type=%r; tok=%r" % (formula, token_type, tok) + msg = ( + "invalid token found in edge specification: %s; token_type=%r; tok=%r" + % (formula, token_type, tok) + ) raise SyntaxError(msg) else: # We are parsing an edge operator @@ -111,10 +115,9 @@ def generate_edges(formula): yield start_names, end_names, arrowheads -def construct_graph_from_formula(cls, formula = None, attr = "name", - simplify = True): +def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): """Graph.Formula(formula = None, attr = "name", simplify = True) - + Generates a graph from a graph formula A graph formula is a simple string representation of a graph. @@ -167,7 +170,7 @@ def construct_graph_from_formula(cls, formula = None, attr = "name", + attr: name (v) + edges (vertex names): A->B - + If you have may disconnected componnets, you can separate them with commas. You can also specify isolated vertices: @@ -199,10 +202,10 @@ def construct_graph_from_formula(cls, formula = None, attr = "name", @param simplify: whether the simplify the constructed graph @return: the constructed graph: """ - + # If we have no formula, return an empty graph if formula is None: - return cls(0, vertex_attrs = {attr: []}) + return cls(0, vertex_attrs={attr: []}) vertex_ids, edges, directed = UniqueIdGenerator(), [], False # Loop over each part in the formula @@ -212,26 +215,23 @@ def construct_graph_from_formula(cls, formula = None, attr = "name", # Parse the first vertex specification from the formula for start_names, end_names, arrowheads in generate_edges(part): start_ids = [vertex_ids[name] for name in start_names] - end_ids = [vertex_ids[name] for name in end_names] + end_ids = [vertex_ids[name] for name in end_names] if not arrowheads[0] and not arrowheads[1]: # This is an undirected edge. Do we have a directed graph? if not directed: # Nope, add the edge - edges.extend((id1, id2) for id1 in start_ids \ - for id2 in end_ids) + edges.extend((id1, id2) for id1 in start_ids for id2 in end_ids) else: # This is a directed edge directed = True if arrowheads[1]: - edges.extend((id1, id2) for id1 in start_ids \ - for id2 in end_ids) + edges.extend((id1, id2) for id1 in start_ids for id2 in end_ids) if arrowheads[0]: - edges.extend((id2, id1) for id1 in start_ids \ - for id2 in end_ids) + edges.extend((id2, id1) for id1 in start_ids for id2 in end_ids) # Grab the vertex names into a list vertex_attrs = {} - vertex_attrs[attr] = vertex_ids.values() + vertex_attrs[attr] = list(vertex_ids.values()) # Construct and return the graph result = cls(len(vertex_ids), edges, directed, vertex_attrs=vertex_attrs) if simplify: diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 7f347b070..1d3fd7623 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -6,13 +6,13 @@ This package contains the implementation of the L{Layout} object. """ -from itertools import izip + from math import sin, cos, pi from igraph.drawing.utils import BoundingBox from igraph.statistics import RunningMean -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -32,6 +32,7 @@ 02110-1301 USA """ + class Layout(object): """Represents the layout of a graph. @@ -64,7 +65,7 @@ class Layout(object): >>> print layout[1] [0, 3] """ - + def __init__(self, coords=None, dim=None): """Constructor. @@ -90,8 +91,10 @@ def __init__(self, coords=None, dim=None): self._dim = int(dim) for row in self._coords: if len(row) != self._dim: - raise ValueError("all items in the coordinate list "+ - "must have a length of %d" % self._dim) + raise ValueError( + "all items in the coordinate list " + + "must have a length of %d" % self._dim + ) def __len__(self): return len(self._coords) @@ -121,8 +124,11 @@ def __repr__(self): dim_count = "1 dimension" else: dim_count = "%d dimensions" % self.dim - return "<%s with %s and %s>" % (self.__class__.__name__, - vertex_count, dim_count) + return "<%s with %s and %s>" % ( + self.__class__.__name__, + vertex_count, + dim_count, + ) @property def dim(self): @@ -138,7 +144,7 @@ def append(self, value): """Appends a new point to the layout""" if len(value) < self._dim: raise ValueError("appended item must have %d elements" % self._dim) - self._coords.append([float(coord) for coord in value[0:self._dim]]) + self._coords.append([float(coord) for coord in value[0 : self._dim]]) def mirror(self, dim): """Mirrors the layout along the given dimension(s) @@ -154,7 +160,6 @@ def mirror(self, dim): for row in self._coords: row[current_dim] *= -1 - def rotate(self, angle, dim1=0, dim2=1, **kwds): """Rotates the layout by the given degrees on the plane defined by the given two dimensions. @@ -166,18 +171,17 @@ def rotate(self, angle, dim1=0, dim2=1, **kwds): origin will be the origin of the coordinate system. """ - origin = list(kwds.get("origin", [0.]*self._dim)) + origin = list(kwds.get("origin", [0.0] * self._dim)) if len(origin) != self._dim: raise ValueError("origin must have %d dimensions" % self._dim) - radian = angle * pi / 180. + radian = angle * pi / 180.0 cos_alpha, sin_alpha = cos(radian), sin(radian) - - for idx, row in enumerate(self._coords): - x, y = row[dim1] - origin[dim1], row[dim2] - origin[dim2] - row[dim1] = cos_alpha*x - sin_alpha*y + origin[dim1] - row[dim2] = sin_alpha*x + cos_alpha*y + origin[dim2] + for idx, row in enumerate(self._coords): + x, y = row[dim1] - origin[dim1], row[dim2] - origin[dim2] + row[dim1] = cos_alpha * x - sin_alpha * y + origin[dim1] + row[dim2] = sin_alpha * x + cos_alpha * y + origin[dim2] def scale(self, *args, **kwds): """Scales the layout. @@ -194,7 +198,7 @@ def scale(self, *args, **kwds): @keyword origin: the origin of scaling (this point will stay in place). Optional, defaults to the origin of the coordinate system being used. """ - origin = list(kwds.get("origin", [0.]*self._dim)) + origin = list(kwds.get("origin", [0.0] * self._dim)) if len(origin) != self._dim: raise ValueError("origin must have %d dimensions" % self._dim) @@ -205,16 +209,16 @@ def scale(self, *args, **kwds): raise ValueError("scaling factor must be given") elif len(scaling) == 1: if type(scaling[0]) == int or type(scaling[0]) == float: - scaling = scaling*self._dim + scaling = scaling * self._dim else: scaling = scaling[0] if len(scaling) != self._dim: - raise ValueError("scaling factor list must have %d elements" \ - % self._dim) + raise ValueError("scaling factor list must have %d elements" % self._dim) for idx, row in enumerate(self._coords): - self._coords[idx] = [(row[d]-origin[d])*scaling[d]+origin[d] \ - for d in xrange(self._dim)] + self._coords[idx] = [ + (row[d] - origin[d]) * scaling[d] + origin[d] for d in range(self._dim) + ] def translate(self, *args, **kwds): """Translates the layout. @@ -233,15 +237,12 @@ def translate(self, *args, **kwds): elif len(v) == 1 and type(v[0]) != int and type(v[0]) != float: v = v[0] if len(v) != self._dim: - raise ValueError("translation vector must have %d dimensions" \ - % self._dim) + raise ValueError("translation vector must have %d dimensions" % self._dim) for idx, row in enumerate(self._coords): - self._coords[idx] = [row[d]+v[d] for d in xrange(self._dim)] - + self._coords[idx] = [row[d] + v[d] for d in range(self._dim)] - def to_radial(self, min_angle = 100, max_angle = 80, \ - min_radius=0.0, max_radius=1.0): + def to_radial(self, min_angle=100, max_angle=80, min_radius=0.0, max_radius=1.0): """Converts a planar layout to a radial one This method applies only to 2D layouts. The X coordinate of the @@ -279,14 +280,13 @@ def to_radial(self, min_angle = 100, max_angle = 80, \ max_angle += 360 ratio_x = (max_angle - min_angle) / bbox.width - ratio_x *= pi / 180. - min_angle *= pi / 180. + ratio_x *= pi / 180.0 + min_angle *= pi / 180.0 ratio_y = (max_radius - min_radius) / bbox.height for idx, (x, y) in enumerate(self._coords): - alpha = (x-bbox.left) * ratio_x + min_angle - radius = (y-bbox.top) * ratio_y + min_radius - self._coords[idx] = [cos(alpha)*radius, -sin(alpha)*radius] - + alpha = (x - bbox.left) * ratio_x + min_angle + radius = (y - bbox.top) * ratio_y + min_radius + self._coords[idx] = [cos(alpha) * radius, -sin(alpha) * radius] def transform(self, function, *args, **kwds): """Performs an arbitrary transformation on the layout @@ -297,20 +297,20 @@ def transform(self, function, *args, **kwds): @param function: a function which receives the coordinates as a tuple and returns the transformed tuple. """ - self._coords = [list(function(tuple(row), *args, **kwds)) \ - for row in self._coords] - + self._coords = [ + list(function(tuple(row), *args, **kwds)) for row in self._coords + ] def centroid(self): """Returns the centroid of the layout. The centroid of the layout is the arithmetic mean of the points in the layout. - + @return: the centroid as a list of floats""" - centroid = [RunningMean() for _ in xrange(self._dim)] + centroid = [RunningMean() for _ in range(self._dim)] for row in self._coords: - for dim in xrange(self._dim): + for dim in range(self._dim): centroid[dim].add(row[dim]) return [rm.mean for rm in centroid] @@ -332,12 +332,12 @@ def boundaries(self, border=0): raise ValueError("layout contains no layout items") mins, maxs = [], [] - for dim in xrange(self._dim): + for dim in range(self._dim): col = [row[dim] for row in self._coords] - mins.append(min(col)-border) - maxs.append(max(col)+border) + mins.append(min(col) - border) + maxs.append(max(col) + border) return mins, maxs - + def bounding_box(self, border=0): """Returns the bounding box of the layout. @@ -360,7 +360,6 @@ def bounding_box(self, border=0): except ValueError: return BoundingBox(0, 0, 0, 0) - def center(self, *args, **kwds): """Centers the layout around the given point. @@ -373,18 +372,15 @@ def center(self, *args, **kwds): the operation.""" center = kwds.get("p") or args if len(center) == 0: - center = [0.] * self._dim - elif len(center) == 1 and type(center[0]) != int \ - and type(center[0]) != float: + center = [0.0] * self._dim + elif len(center) == 1 and type(center[0]) != int and type(center[0]) != float: center = center[0] if len(center) != self._dim: - raise ValueError("the given point must have %d dimensions" \ - % self._dim) + raise ValueError("the given point must have %d dimensions" % self._dim) centroid = self.centroid() - vec = [center[d]-centroid[d] for d in xrange(self._dim)] + vec = [center[d] - centroid[d] for d in range(self._dim)] self.translate(vec) - def copy(self): """Creates an exact copy of the layout.""" return self.__copy__() @@ -409,20 +405,21 @@ def fit_into(self, bbox, keep_aspect_ratio=True): raise TypeError("bounding boxes work for 2D layouts only") corner, target_sizes = [bbox.left, bbox.top], [bbox.width, bbox.height] elif len(bbox) == self._dim: - corner, target_sizes = [0.] * self._dim, list(bbox) + corner, target_sizes = [0.0] * self._dim, list(bbox) elif len(bbox) == 2 * self._dim: - corner, opposite_corner = list(bbox[0:self._dim]), list(bbox[self._dim:]) - for i in xrange(self._dim): + corner, opposite_corner = list(bbox[0 : self._dim]), list(bbox[self._dim :]) + for i in range(self._dim): if corner[i] > opposite_corner[i]: corner[i], opposite_corner[i] = opposite_corner[i], corner[i] - target_sizes = [max_val-min_val \ - for min_val, max_val in izip(corner, opposite_corner)] + target_sizes = [ + max_val - min_val for min_val, max_val in zip(corner, opposite_corner) + ] try: mins, maxs = self.boundaries() except ValueError: mins, maxs = [0.0] * self._dim, [0.0] * self._dim - sizes = [max_val - min_val for min_val, max_val in izip(mins, maxs)] + sizes = [max_val - min_val for min_val, max_val in zip(mins, maxs)] for i, size in enumerate(sizes): if size == 0: @@ -430,18 +427,19 @@ def fit_into(self, bbox, keep_aspect_ratio=True): mins[i] -= 1 maxs[i] += 1 - ratios = [float(target_size) / current_size \ - for current_size, target_size in izip(sizes, target_sizes)] + ratios = [ + float(target_size) / current_size + for current_size, target_size in zip(sizes, target_sizes) + ] if keep_aspect_ratio: min_ratio = min(ratios) ratios = [min_ratio] * self._dim translations = [] - for i in xrange(self._dim): - trans = (target_sizes[i] - ratios[i] * sizes[i]) / 2. + for i in range(self._dim): + trans = (target_sizes[i] - ratios[i] * sizes[i]) / 2.0 trans -= mins[i] * ratios[i] - corner[i] translations.append(trans) self.scale(*ratios) self.translate(*translations) - diff --git a/src/igraph/matching.py b/src/igraph/matching.py index 0e75ad9d2..85d40d226 100644 --- a/src/igraph/matching.py +++ b/src/igraph/matching.py @@ -5,7 +5,7 @@ from igraph.clustering import VertexClustering from igraph._igraph import Vertex -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -25,6 +25,7 @@ 02110-1301 USA """ + class Matching(object): """A matching of vertices in a graph. @@ -65,7 +66,7 @@ def __init__(self, graph, matching, types=None): self._num_matched = 0 self._types = None - if isinstance(types, basestring): + if isinstance(types, str): types = graph.vs[types] self.types = types @@ -76,11 +77,14 @@ def __len__(self): def __repr__(self): if self._types is not None: - return "%s(%r,%r,types=%r)" % \ - (self.__class__.__name__, self._graph, self._matching, self._types) + return "%s(%r,%r,types=%r)" % ( + self.__class__.__name__, + self._graph, + self._matching, + self._types, + ) else: - return "%s(%r,%r)" % \ - (self.__class__.__name__, self._graph, self._matching) + return "%s(%r,%r)" % (self.__class__.__name__, self._graph, self._matching) def __str__(self): if self._types is not None: @@ -95,8 +99,11 @@ def edges(self): of them will be returned. """ get_eid = self._graph.get_eid - eidxs = [get_eid(u, v, directed=False) \ - for u, v in enumerate(self._matching) if v != -1 and u <= v] + eidxs = [ + get_eid(u, v, directed=False) + for u, v in enumerate(self._matching) + if v != -1 and u <= v + ] return self._graph.es[eidxs] @property @@ -121,7 +128,7 @@ def is_matched(self, vertex): def match_of(self, vertex): """Returns the vertex a given vertex is matched to. - + @param vertex: the vertex we are interested in; either an integer index or an instance of L{Vertex}. @return: the index of the vertex matched to the given vertex, either as diff --git a/src/igraph/operators.py b/src/igraph/operators.py index 00f8e0c13..a8c19a40d 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -2,11 +2,10 @@ # -*- coding: utf-8 -*- """Implementation of union, disjoint union and intersection operators.""" -from __future__ import with_statement __all__ = ("disjoint_union", "union", "intersection") __docformat__ = "restructuredtext en" -__license__ = u""" +__license__ = """ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -28,7 +27,6 @@ from igraph._igraph import GraphBase, _union, _intersection, _disjoint_union -from collections import defaultdict, Counter from warnings import warn @@ -49,7 +47,7 @@ def disjoint_union(graphs): @return: the disjoint union graph """ if any(not isinstance(g, GraphBase) for g in graphs): - raise TypeError('Not all elements are graphs') + raise TypeError("Not all elements are graphs") ngr = len(graphs) # Trivial cases @@ -81,16 +79,15 @@ def disjoint_union(graphs): # New conflict a_conflict.add(a_name) igf = a_first_graph[a_name] - graph_union['{:}_{:}'.format(a_name, igf)] = \ - graph_union.pop(a_name) - graph_union['{:}_{:}'.format(a_name, ig)] = a_value + graph_union["{:}_{:}".format(a_name, igf)] = graph_union.pop(a_name) + graph_union["{:}_{:}".format(a_name, ig)] = a_value # Vertex attributes i = 0 for g in graphs: nv = g.vcount() for attr in g.vertex_attributes(): - graph_union.vs[i: i + nv][attr] = g.vs[attr] + graph_union.vs[i : i + nv][attr] = g.vs[attr] i += nv # Edge attributes @@ -98,13 +95,13 @@ def disjoint_union(graphs): for g in graphs: ne = g.ecount() for attr in g.edge_attributes(): - graph_union.es[i: i + ne][attr] = g.es[attr] + graph_union.es[i : i + ne][attr] = g.es[attr] i += ne return graph_union -def union(graphs, byname='auto'): +def union(graphs, byname="auto"): """Graph union. The union of two or more graphs is created. The graphs may have identical @@ -133,14 +130,14 @@ def union(graphs, byname='auto'): """ if any(not isinstance(g, GraphBase) for g in graphs): - raise TypeError('Not all elements are graphs') + raise TypeError("Not all elements are graphs") - if byname not in (True, False, 'auto'): + if byname not in (True, False, "auto"): raise ValueError('"byname" should be a bool or "auto"') ngr = len(graphs) n_named = sum(g.is_named() for g in graphs) - if byname == 'auto': + if byname == "auto": byname = n_named == ngr if n_named not in (0, ngr): warn("Some, but not all graphs are named, not using vertex names") @@ -156,7 +153,7 @@ def union(graphs, byname='auto'): # Now there are at least two graphs if byname: - allnames = [g.vs['name'] for g in graphs] + allnames = [g.vs["name"] for g in graphs] uninames = list(set.union(*(set(vns) for vns in allnames))) permutation_map = {x: i for i, x in enumerate(uninames)} nve = len(uninames) @@ -171,7 +168,7 @@ def union(graphs, byname='auto'): # Reorder vertices to match uninames # vertex k -> p[k] - permutation = [permutation_map[x] for x in ng.vs['name']] + permutation = [permutation_map[x] for x in ng.vs["name"]] ng = ng.permute_vertices(permutation) newgraphs.append(ng) @@ -182,8 +179,8 @@ def union(graphs, byname='auto'): edgemaps = any(len(g.edge_attributes()) for g in graphs) res = _union(newgraphs, edgemaps) if edgemaps: - graph_union = res['graph'] - edgemaps = res['edgemaps'] + graph_union = res["graph"] + edgemaps = res["edgemaps"] else: graph_union = res @@ -205,14 +202,13 @@ def union(graphs, byname='auto'): # New conflict a_conflict.add(a_name) igf = a_first_graph[a_name] - graph_union['{:}_{:}'.format(a_name, igf)] = \ - graph_union.pop(a_name) - graph_union['{:}_{:}'.format(a_name, ig)] = a_value + graph_union["{:}_{:}".format(a_name, igf)] = graph_union.pop(a_name) + graph_union["{:}_{:}".format(a_name, ig)] = a_value # Vertex attributes if byname: - graph_union.vs['name'] = uninames - attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - set(['name']) + graph_union.vs["name"] = uninames + attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - set(["name"]) nve = graph_union.vcount() for a_name in attrs: # Check for conflicts at at least one vertex @@ -239,7 +235,7 @@ def union(graphs, byname='auto'): # There is a conflict, name after the graph number for ig, g in enumerate(newgraphs, 1): if a_name in g.vertex_attributes(): - graph_union.vs['{:}_{:}'.format(a_name, ig)] = g.vs[a_name] + graph_union.vs["{:}_{:}".format(a_name, ig)] = g.vs[a_name] # Edge attributes if edgemaps: @@ -259,7 +255,7 @@ def union(graphs, byname='auto'): vals[iu] = a_value continue if vals[iu] != a_value: - print(g, g.vs['name'], emap, a_value, iu, vals[iu]) + print(g, g.vs["name"], emap, a_value, iu, vals[iu]) conflict = True break if conflict: @@ -277,12 +273,12 @@ def union(graphs, byname='auto'): vals = [None for i in range(ne)] for iu, a_value in zip(emap, g.es[a_name]): vals[iu] = a_value - graph_union.es['{:}_{:}'.format(a_name, ig)] = vals + graph_union.es["{:}_{:}".format(a_name, ig)] = vals return graph_union -def intersection(graphs, byname='auto', keep_all_vertices=True): +def intersection(graphs, byname="auto", keep_all_vertices=True): """Graph intersection. The intersection of two or more graphs is created. The graphs may have @@ -313,14 +309,14 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): """ if any(not isinstance(g, GraphBase) for g in graphs): - raise TypeError('Not all elements are graphs') + raise TypeError("Not all elements are graphs") - if byname not in (True, False, 'auto'): + if byname not in (True, False, "auto"): raise ValueError('"byname" should be a bool or "auto"') ngr = len(graphs) n_named = sum(g.is_named() for g in graphs) - if byname == 'auto': + if byname == "auto": byname = n_named == ngr if n_named not in (0, ngr): warn("Some, but not all graphs are named, not using vertex names") @@ -336,7 +332,7 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): # Now there are at least two graphs if byname: - allnames = [g.vs['name'] for g in graphs] + allnames = [g.vs["name"] for g in graphs] if keep_all_vertices: uninames = list(set.union(*(set(vns) for vns in allnames))) @@ -361,7 +357,7 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): # Reorder vertices to match uninames # vertex k -> p[k] - permutation = [permutation_map[x] for x in ng.vs['name']] + permutation = [permutation_map[x] for x in ng.vs["name"]] ng = ng.permute_vertices(permutation) newgraphs.append(ng) @@ -372,8 +368,8 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): edgemaps = any(len(g.edge_attributes()) for g in graphs) res = _intersection(newgraphs, edgemaps) if edgemaps: - graph_intsec = res['graph'] - edgemaps = res['edgemaps'] + graph_intsec = res["graph"] + edgemaps = res["edgemaps"] else: graph_intsec = res @@ -395,14 +391,13 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): # New conflict a_conflict.add(a_name) igf = a_first_graph[a_name] - graph_intsec['{:}_{:}'.format(a_name, igf)] = \ - graph_intsec.pop(a_name) - graph_intsec['{:}_{:}'.format(a_name, ig)] = a_value + graph_intsec["{:}_{:}".format(a_name, igf)] = graph_intsec.pop(a_name) + graph_intsec["{:}_{:}".format(a_name, ig)] = a_value # Vertex attributes if byname: - graph_intsec.vs['name'] = uninames - attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - set(['name']) + graph_intsec.vs["name"] = uninames + attrs = set.union(*(set(g.vertex_attributes()) for g in newgraphs)) - set(["name"]) nv = graph_intsec.vcount() for a_name in attrs: # Check for conflicts at at least one vertex @@ -430,7 +425,7 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): # There is a conflict, name after the graph number for ig, g in enumerate(newgraphs, 1): if a_name in g.vertex_attributes(): - graph_intsec.vs['{:}_{:}'.format(a_name, ig)] = g.vs[a_name] + graph_intsec.vs["{:}_{:}".format(a_name, ig)] = g.vs[a_name] # Edge attributes if edgemaps: @@ -471,6 +466,6 @@ def intersection(graphs, byname='auto', keep_all_vertices=True): if iu == -1: continue vals[iu] = a_value - graph_intsec.es['{:}_{:}'.format(a_name, ig)] = vals + graph_intsec.es["{:}_{:}".format(a_name, ig)] = vals return graph_intsec diff --git a/src/igraph/remote/gephi.py b/src/igraph/remote/gephi.py index adb362a60..67f48c90e 100644 --- a/src/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -3,7 +3,7 @@ """Classes that help igraph communicate with Gephi (http://www.gephi.org).""" from igraph.compat import property -import urllib2 +import urllib.request, urllib.error, urllib.parse try: # JSON is optional so we don't blow up with Python < 2.6 @@ -15,11 +15,12 @@ except ImportError: # No simplejson either from igraph.drawing.utils import FakeModule + json = FakeModule() __all__ = ["GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat"] __docformat__ = "restructuredtext en" -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -66,7 +67,7 @@ def __init__(self, url=None, host="127.0.0.1", port=8080, workspace=1): def __del__(self): try: self.close() - except urllib2.URLError: + except urllib.error.URLError: # Happens when Gephi is closed before the connection is destroyed pass @@ -83,7 +84,7 @@ def flush(self): single request.""" data = b"".join(self._pending_operations) self._pending_operations = [] - conn = urllib2.urlopen(self._update_url, data=data) + conn = urllib.request.urlopen(self._update_url, data=data) return conn.read() @property @@ -241,7 +242,7 @@ def iterjsonobj(self, graph): to the destination.""" # Construct a unique ID prefix - id_prefix = "igraph:%s" % (hex(id(graph)), ) + id_prefix = "igraph:%s" % (hex(id(graph)),) # Add the vertices add_node = self.format.get_add_node_event @@ -252,10 +253,13 @@ def iterjsonobj(self, graph): add_edge = self.format.get_add_edge_event directed = graph.is_directed() for edge in graph.es: - yield add_edge("%s:e:%d:%d" % (id_prefix, edge.source, edge.target), - "%s:v:%d" % (id_prefix, edge.source), - "%s:v:%d" % (id_prefix, edge.target), - directed, edge.attributes()) + yield add_edge( + "%s:e:%d:%d" % (id_prefix, edge.source, edge.target), + "%s:v:%d" % (id_prefix, edge.source), + "%s:v:%d" % (id_prefix, edge.target), + directed, + edge.attributes(), + ) def post(self, graph, destination, encoder=None): """Posts the given graph to the destination of the streamer using the @@ -285,4 +289,3 @@ def send_event(self, event, destination, encoder=None, flush=True): destination.write(b"\r\n") if flush: destination.flush() - diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 15a0e1aa5..b62199244 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -4,7 +4,7 @@ Statistics related stuff in igraph """ -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamas Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -26,8 +26,16 @@ import math -__all__ = ["FittedPowerLaw", "Histogram", "RunningMean", "mean", "median", \ - "percentile", "quantile", "power_law_fit"] +__all__ = [ + "FittedPowerLaw", + "Histogram", + "RunningMean", + "mean", + "median", + "percentile", + "quantile", + "power_law_fit", +] class FittedPowerLaw(object): @@ -38,7 +46,7 @@ class FittedPowerLaw(object): >>> result = power_law_fit([1, 2, 3, 4, 5, 6]) >>> result # doctest:+ELLIPSIS FittedPowerLaw(continuous=False, alpha=2.425828..., xmin=3.0, L=-7.54633..., D=0.2138..., p=0.99311...) - >>> print result # doctest:+ELLIPSIS + >>> print(result) # doctest:+ELLIPSIS Fitted power-law distribution on discrete data Exponent (alpha) = 2.425828 @@ -69,9 +77,15 @@ def __init__(self, continuous, alpha, xmin, L, D, p): self.p = p def __repr__(self): - return "%s(continuous=%r, alpha=%r, xmin=%r, L=%r, D=%r, p=%r)" % \ - (self.__class__.__name__, self.continuous, self.alpha, \ - self.xmin, self.L, self.D, self.p) + return "%s(continuous=%r, alpha=%r, xmin=%r, L=%r, D=%r, p=%r)" % ( + self.__class__.__name__, + self.continuous, + self.alpha, + self.xmin, + self.L, + self.D, + self.p, + ) def __str__(self): return self.summary(significance=0.05) @@ -84,8 +98,10 @@ def summary(self, significance=0.05): distribution @return: the summary as a string """ - result = ["Fitted power-law distribution on %s data" % \ - ("discrete", "continuous")[bool(self.continuous)]] + result = [ + "Fitted power-law distribution on %s data" + % ("discrete", "continuous")[bool(self.continuous)] + ] result.append("") result.append("Exponent (alpha) = %f" % self.alpha) result.append("Cutoff (xmin) = %f" % self.xmin) @@ -98,29 +114,29 @@ def summary(self, significance=0.05): result.append("p-value = %f" % self.p) result.append("") if self.p < significance: - result.append("H0 rejected at significance level %g" \ - % significance) + result.append("H0 rejected at significance level %g" % significance) else: - result.append("H0 could not be rejected at significance "\ - "level %g" % significance) + result.append( + "H0 could not be rejected at significance " "level %g" % significance + ) return "\n".join(result) class Histogram(object): """Generic histogram class for real numbers - + Example: - + >>> h = Histogram(5) # Initializing, bin width = 5 >>> h << [2,3,2,7,8,5,5,0,7,9] # Adding more items - >>> print h + >>> print(h) N = 10, mean +- sd: 4.8000 +- 2.9740 [ 0, 5): **** (4) [ 5, 10): ****** (6) """ - def __init__(self, bin_width = 1, data = None): + def __init__(self, bin_width=1, data=None): """Initializes the histogram with the given data set. @param bin_width: the bin width of the histogram. @@ -135,7 +151,7 @@ def __init__(self, bin_width = 1, data = None): if data: self.add_many(data) - def _get_bin(self, num, create = False): + def _get_bin(self, num, create=False): """Returns the bin index corresponding to the given number. @param num: the number for which the bin is being sought @@ -145,31 +161,31 @@ def _get_bin(self, num, create = False): if len(self._bins) == 0: if not create: result = None - else: - self._min = int(num/self._bin_width)*self._bin_width - self._max = self._min+self._bin_width + else: + self._min = int(num / self._bin_width) * self._bin_width + self._max = self._min + self._bin_width self._bins = [0] result = 0 return result if num >= self._min: - binidx = int((num-self._min)/self._bin_width) + binidx = int((num - self._min) / self._bin_width) if binidx < len(self._bins): return binidx if not create: return None - extra_bins = binidx-len(self._bins)+1 - self._bins.extend([0]*extra_bins) - self._max = self._min + len(self._bins)*self._bin_width + extra_bins = binidx - len(self._bins) + 1 + self._bins.extend([0] * extra_bins) + self._max = self._min + len(self._bins) * self._bin_width return binidx if not create: return None - extra_bins = int(math.ceil((self._min-num)/self._bin_width)) - self._bins[0:0] = [0]*extra_bins - self._min -= extra_bins*self._bin_width - self._max = self._min + len(self._bins)*self._bin_width + extra_bins = int(math.ceil((self._min - num) / self._bin_width)) + self._bins[0:0] = [0] * extra_bins + self._min -= extra_bins * self._bin_width + self._max = self._min + len(self._bins) * self._bin_width return 0 @property @@ -196,13 +212,13 @@ def var(self): def add(self, num, repeat=1): """Adds a single number to the histogram. - + @param num: the number to be added @param repeat: number of repeated additions """ num = float(num) binidx = self._get_bin(num, True) - self._bins[binidx] += repeat + self._bins[binidx] += repeat self._running_mean.add(num, repeat) def add_many(self, data): @@ -215,6 +231,7 @@ def add_many(self, data): iterator = iter([data]) for x in iterator: self.add(x) + __lshift__ = add_many def clear(self): @@ -225,33 +242,43 @@ def clear(self): def bins(self): """Generator returning the bins of the histogram in increasing order - + @return: a tuple with the following elements: left bound, right bound, number of elements in the bin""" x = self._min for elem in self._bins: - yield (x, x+self._bin_width, elem) + yield (x, x + self._bin_width, elem) x += self._bin_width def __plot__(self, context, bbox, _, **kwds): """Plotting support""" from igraph.drawing.coord import DescartesCoordinateSystem - coord_system = DescartesCoordinateSystem(context, bbox, \ - (kwds.get("min", self._min), 0, \ - kwds.get("max", self._max), kwds.get("max_value", max(self._bins)) - )) + + coord_system = DescartesCoordinateSystem( + context, + bbox, + ( + kwds.get("min", self._min), + 0, + kwds.get("max", self._max), + kwds.get("max_value", max(self._bins)), + ), + ) # Draw the boxes context.set_line_width(1) - context.set_source_rgb(1., 0., 0.) + context.set_source_rgb(1.0, 0.0, 0.0) x = self._min for value in self._bins: top_left_x, top_left_y = coord_system.local_to_context(x, value) x += self._bin_width bottom_right_x, bottom_right_y = coord_system.local_to_context(x, 0) - context.rectangle(top_left_x, top_left_y, \ - bottom_right_x - top_left_x, \ - bottom_right_y - top_left_y) + context.rectangle( + top_left_x, + top_left_y, + bottom_right_x - top_left_x, + bottom_right_y - top_left_y, + ) context.fill() # Draw the axes @@ -277,8 +304,7 @@ def to_string(self, max_width=78, show_bars=True, show_counts=True): number_format = "%d" else: number_format = "%.3f" - num_length = max(len(number_format % self._min), \ - len(number_format % self._max)) + num_length = max(len(number_format % self._min), len(number_format % self._max)) number_format = "%" + str(num_length) + number_format[1:] format_string = "[%s, %s): %%s" % (number_format, number_format) @@ -287,13 +313,12 @@ def to_string(self, max_width=78, show_bars=True, show_counts=True): maxval = max(self._bins) if show_counts: maxval_length = len(str(maxval)) - scale = maxval // (max_width-2*num_length-maxval_length-9) + scale = maxval // (max_width - 2 * num_length - maxval_length - 9) else: - scale = maxval // (max_width-2*num_length-6) + scale = maxval // (max_width - 2 * num_length - 6) scale = max(scale, 1) - result = ["N = %d, mean +- sd: %.4f +- %.4f" % \ - (self.n, self.mean, self.sd)] + result = ["N = %d, mean +- sd: %.4f +- %.4f" % (self.n, self.mean, self.sd)] if show_bars: # Print the bars @@ -302,10 +327,12 @@ def to_string(self, max_width=78, show_bars=True, show_counts=True): if show_counts: format_string += " (%d)" for left, right, cnt in self.bins(): - result.append(format_string % (left, right, '*'*(cnt//scale), cnt)) + result.append( + format_string % (left, right, "*" * (cnt // scale), cnt) + ) else: for left, right, cnt in self.bins(): - result.append(format_string % (left, right, '*'*(cnt//scale))) + result.append(format_string % (left, right, "*" * (cnt // scale))) elif show_counts: # Print the counts only for left, right, cnt in self.bins(): @@ -317,10 +344,9 @@ def __str__(self): return self.to_string() - class RunningMean(object): """Running mean calculator. - + This class can be used to calculate the mean of elements from a list, tuple, iterable or any other data source. The mean is calculated on the fly without explicitly summing the values, @@ -332,9 +358,9 @@ class RunningMean(object): # pylint: disable-msg=C0103 def __init__(self, items=None, n=0.0, mean=0.0, sd=0.0): """RunningMean(items=None, n=0.0, mean=0.0, sd=0.0) - + Initializes the running mean calculator. - + There are two possible ways to initialize the calculator. First, one can provide an iterable of items; alternatively, one can specify the number of items, the mean and the @@ -359,15 +385,15 @@ def __init__(self, items=None, n=0.0, mean=0.0, sd=0.0): self._nitems = float(n) self._mean = float(mean) if n > 1: - self._sqdiff = float(sd) ** 2 * float(n-1) + self._sqdiff = float(sd) ** 2 * float(n - 1) self._sd = float(sd) else: self._sqdiff = 0.0 self._sd = 0.0 - + def add(self, value, repeat=1): """RunningMean.add(value, repeat=1) - + Adds the given value to the elements from which we calculate the mean and the standard deviation. @@ -377,24 +403,24 @@ def add(self, value, repeat=1): repeat = int(repeat) self._nitems += repeat delta = value - self._mean - self._mean += (repeat*delta / self._nitems) - self._sqdiff += (repeat*delta) * (value - self._mean) + self._mean += repeat * delta / self._nitems + self._sqdiff += (repeat * delta) * (value - self._mean) if self._nitems > 1: - self._sd = (self._sqdiff / (self._nitems-1)) ** 0.5 + self._sd = (self._sqdiff / (self._nitems - 1)) ** 0.5 def add_many(self, values): """RunningMean.add(values) - + Adds the values in the given iterable to the elements from which we calculate the mean. Can also accept a single number. The left shift (C{<<}) operator is aliased to this function, so you can use it to add elements as well: - + >>> rm=RunningMean() - >>> rm << [1,2,3,4] + >>> rm << [1,2,3,4] >>> rm.result # doctest:+ELLIPSIS (2.5, 1.290994...) - + @param values: the element(s) to be added @type values: iterable""" try: @@ -430,16 +456,18 @@ def var(self): return self._sd ** 2 def __repr__(self): - return "%s(n=%r, mean=%r, sd=%r)" % \ - (self.__class__.__name__, int(self._nitems), - self._mean, self._sd) + return "%s(n=%r, mean=%r, sd=%r)" % ( + self.__class__.__name__, + int(self._nitems), + self._mean, + self._sd, + ) def __str__(self): - return "Running mean (N=%d, %f +- %f)" % \ - (self._nitems, self._mean, self._sd) - + return "Running mean (N=%d, %f +- %f)" % (self._nitems, self._mean, self._sd) + __lshift__ = add_many - + def __float__(self): return float(self._mean) @@ -447,7 +475,7 @@ def __int__(self): return int(self._mean) def __long__(self): - return long(self._mean) + return int(self._mean) def __complex__(self): return complex(self._mean) @@ -471,6 +499,7 @@ def mean(xs): """ return RunningMean(xs).mean + def median(xs, sort=True): """Returns the median of an unsorted or sorted numeric vector. @@ -485,10 +514,11 @@ def median(xs, sort=True): mid = int(len(xs) / 2) if 2 * mid == len(xs): - return float(xs[mid-1] + xs[mid]) / 2 + return float(xs[mid - 1] + xs[mid]) / 2 else: return float(xs[mid]) + def percentile(xs, p=(25, 50, 75), sort=True): """Returns the pth percentile of an unsorted or sorted numeric vector. @@ -500,7 +530,7 @@ def percentile(xs, p=(25, 50, 75), sort=True): >>> round(percentile([15, 20, 40, 35, 50], 40), 2) 26.0 >>> for perc in percentile([15, 20, 40, 35, 50], (0, 25, 50, 75, 100)): - ... print "%.2f" % perc + ... print("%.2f" % perc) ... 15.00 17.50 @@ -519,8 +549,9 @@ def percentile(xs, p=(25, 50, 75), sort=True): list containing the percentiles for each item in the list. """ if hasattr(p, "__iter__"): - return quantile(xs, (x/100.0 for x in p), sort) - return quantile(xs, p/100.0, sort) + return quantile(xs, (x / 100.0 for x in p), sort) + return quantile(xs, p / 100.0, sort) + def power_law_fit(data, xmin=None, method="auto", return_alpha_only=False): """Fitting a power-law distribution to empirical data @@ -554,7 +585,7 @@ def power_law_fit(data, xmin=None, method="auto", return_alpha_only=False): @return: a L{FittedPowerLaw} object. The fitted C{xmin} value and the power-law exponent can be queried from the C{xmin} and C{alpha} properties of the returned object. - + @newfield ref: Reference @ref: MEJ Newman: Power laws, Pareto distributions and Zipf's law. Contemporary Physics 46, 323-351 (2005) @@ -573,12 +604,16 @@ def power_law_fit(data, xmin=None, method="auto", return_alpha_only=False): fit = FittedPowerLaw(*_power_law_fit(data, xmin, force_continuous)) if return_alpha_only: from igraph import deprecated - deprecated("The return_alpha_only keyword argument of power_law_fit is "\ - "deprecated from igraph 0.7 and will be removed in igraph 0.8") + + deprecated( + "The return_alpha_only keyword argument of power_law_fit is " + "deprecated from igraph 0.7 and will be removed in igraph 0.8" + ) return fit.alpha else: return fit + def quantile(xs, q=(0.25, 0.5, 0.75), sort=True): """Returns the qth quantile of an unsorted or sorted numeric vector. @@ -622,18 +657,19 @@ def quantile(xs, q=(0.25, 0.5, 0.75), sort=True): for q in qs: if q < 0 or q > 1: raise ValueError("q must be between 0 and 1") - n = float(q) * (len(xs)+1) - k, d = int(n), n-int(n) + n = float(q) * (len(xs) + 1) + k, d = int(n), n - int(n) if k >= len(xs): result.append(xs[-1]) elif k < 1: result.append(xs[0]) else: - result.append((1-d) * xs[k-1] + d * xs[k]) + result.append((1 - d) * xs[k - 1] + d * xs[k]) if return_single: result = result[0] return result + def sd(xs): """Returns the standard deviation of an iterable. @@ -649,6 +685,7 @@ def sd(xs): """ return RunningMean(xs).sd + def var(xs): """Returns the variance of an iterable. diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 143b597b0..2fda39ab0 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -13,7 +13,7 @@ __all__ = ["GraphSummary"] -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -33,6 +33,7 @@ 02110-1301 USA """ + class FakeWrapper(object): """Object whose interface is compatible with C{textwrap.TextWrapper} but does no wrapping.""" @@ -46,6 +47,7 @@ def fill(self, text): def wrap(self, text): return [text] + def _get_wrapper_for_width(width, *args, **kwds): """Returns a text wrapper that wraps text for the given width. @@ -56,6 +58,7 @@ def _get_wrapper_for_width(width, *args, **kwds): return FakeWrapper(*args, **kwds) return TextWrapper(width=width, *args, **kwds) + class GraphSummary(object): """Summary representation of a graph. @@ -87,14 +90,18 @@ class GraphSummary(object): _infer_column_alignment, _new_table, _vertex_attribute_iterator """ - - def __init__(self, graph, verbosity=0, width=78, - edge_list_format="auto", - max_rows=99999, - print_graph_attributes=False, - print_vertex_attributes=False, - print_edge_attributes=False, - full=False): + def __init__( + self, + graph, + verbosity=0, + width=78, + edge_list_format="auto", + max_rows=99999, + print_graph_attributes=False, + print_vertex_attributes=False, + print_edge_attributes=False, + full=False, + ): """Constructs a summary representation of a graph. @param verbosity: the verbosity of the summary. If zero, only @@ -131,8 +138,7 @@ def __init__(self, graph, verbosity=0, width=78, self.print_edge_attributes = print_edge_attributes self.verbosity = verbosity self.width = width - self.wrapper = _get_wrapper_for_width(self.width, - break_on_hyphens=False) + self.wrapper = _get_wrapper_for_width(self.width, break_on_hyphens=False) if self._graph.is_named(): self._edges_header = "+ edges (vertex names):" @@ -162,7 +168,7 @@ def _construct_edgelist_adjlist(self): maxlen = len(str(self._graph.vcount())) num_format = "%%%dd" % maxlen format_str = "%s %s %%s" % (num_format, self._arrow) - for v1 in xrange(self._graph.vcount()): + for v1 in range(self._graph.vcount()): neis = self._graph.successors(v1) neis = " ".join(num_format % v2 for v2 in neis) result.append(format_str % (v1, neis)) @@ -175,7 +181,7 @@ def _construct_edgelist_adjlist(self): # Rewrap to multiple columns nrows = len(result) - 1 colheight = int(ceil(nrows / float(colcount))) - newrows = [[] for _ in xrange(colheight)] + newrows = [[] for _ in range(colheight)] for i, row in enumerate(result[1:]): newrows[i % colheight].append(row.ljust(maxlen)) result[1:] = [" ".join(row) for row in newrows] @@ -190,8 +196,10 @@ def _construct_edgelist_compressed(self): if self._graph.is_named(): names = self._graph.vs["name"] - edges = ", ".join(arrow % (names[edge.source], names[edge.target]) - for edge in self._graph.es) + edges = ", ".join( + arrow % (names[edge.source], names[edge.target]) + for edge in self._graph.es + ) else: edges = " ".join(arrow % edge.tuple for edge in self._graph.es) @@ -204,9 +212,12 @@ def _construct_edgelist_edgelist(self): attrs = sorted(self._graph.edge_attributes()) table = self._new_table(headers=["", "edge"] + attrs) - table.add_rows(islice(self._edge_attribute_iterator(attrs), 0, self.max_rows), - header=False) - table.set_cols_align(["l", "l"] + self._infer_column_alignment(edge_attrs=attrs)) + table.add_rows( + islice(self._edge_attribute_iterator(attrs), 0, self.max_rows), header=False + ) + table.set_cols_align( + ["l", "l"] + self._infer_column_alignment(edge_attrs=attrs) + ) result = [self._edges_header] result.extend(table.draw().split("\n")) @@ -222,7 +233,7 @@ def _construct_graph_attributes(self): result = ["+ graph attributes:"] attrs.sort() for attr in attrs: - result.append("[[%s]]" % (attr, )) + result.append("[[%s]]" % (attr,)) result.append(str(self._graph[attr])) return result @@ -233,8 +244,10 @@ def _construct_vertex_attributes(self): return [] table = self._new_table(headers=[""] + attrs) - table.add_rows(islice(self._vertex_attribute_iterator(attrs), 0, self.max_rows), - header=False) + table.add_rows( + islice(self._vertex_attribute_iterator(attrs), 0, self.max_rows), + header=False, + ) table.set_cols_align(["l"] + self._infer_column_alignment(vertex_attrs=attrs)) result = ["+ vertex attributes:"] @@ -246,29 +259,31 @@ def _construct_header(self): """Constructs the header part of the summary.""" graph = self._graph params = dict( - directed="UD"[graph.is_directed()], - named="-N"[graph.is_named()], - weighted="-W"[graph.is_weighted()], - typed="-T"["type" in graph.vertex_attributes()], - vcount=graph.vcount(), - ecount=graph.ecount(), + directed="UD"[graph.is_directed()], + named="-N"[graph.is_named()], + weighted="-W"[graph.is_weighted()], + typed="-T"["type" in graph.vertex_attributes()], + vcount=graph.vcount(), + ecount=graph.ecount(), ) if "name" in graph.attributes(): params["name"] = graph["name"] else: params["name"] = "" - result = ["IGRAPH %(directed)s%(named)s%(weighted)s%(typed)s "\ - "%(vcount)d %(ecount)d -- %(name)s" % params] - - attrs = ["%s (g)" % (name, ) for name in sorted(graph.attributes())] - attrs.extend("%s (v)" % (name, ) for name in sorted(graph.vertex_attributes())) - attrs.extend("%s (e)" % (name, ) for name in sorted(graph.edge_attributes())) + result = [ + "IGRAPH %(directed)s%(named)s%(weighted)s%(typed)s " + "%(vcount)d %(ecount)d -- %(name)s" % params + ] + + attrs = ["%s (g)" % (name,) for name in sorted(graph.attributes())] + attrs.extend("%s (v)" % (name,) for name in sorted(graph.vertex_attributes())) + attrs.extend("%s (e)" % (name,) for name in sorted(graph.edge_attributes())) if attrs: result.append("+ attr: %s" % ", ".join(attrs)) if self.wrapper is not None: - self.wrapper.subsequent_indent = ' ' + self.wrapper.subsequent_indent = " " result[-1:] = self.wrapper.wrap(result[-1]) - self.wrapper.subsequent_indent = '' + self.wrapper.subsequent_indent = "" return result @@ -282,13 +297,15 @@ def _edge_attribute_iterator(self, attribute_order): names = self._graph.vs["name"] for edge in self._graph.es: formatted_edge = arrow % (names[edge.source], names[edge.target]) - yield ["[%d]" % edge.index, formatted_edge] + \ - [edge[attr] for attr in attribute_order] + yield ["[%d]" % edge.index, formatted_edge] + [ + edge[attr] for attr in attribute_order + ] else: for edge in self._graph.es: formatted_edge = arrow % edge.tuple - yield ["[%d]" % edge.index, formatted_edge] + \ - [edge[attr] for attr in attribute_order] + yield ["[%d]" % edge.index, formatted_edge] + [ + edge[attr] for attr in attribute_order + ] def _infer_column_alignment(self, vertex_attrs=None, edge_attrs=None): """Infers the preferred alignment for the given vertex and edge attributes @@ -347,7 +364,7 @@ def __str__(self): if self._graph.ecount() > 0: # Add the edge list if self.edge_list_format == "auto": - if (self.print_edge_attributes and self._graph.edge_attributes()): + if self.print_edge_attributes and self._graph.edge_attributes(): format = "edgelist" elif median(self._graph.degree(mode="out")) < 3: format = "compressed" @@ -364,4 +381,3 @@ def __str__(self): return "\n".join("\n".join(self.wrapper.wrap(line)) for line in output) return "\n".join(output) - diff --git a/src/igraph/test/foreign.py b/src/igraph/test/foreign.py index af8c1c950..eabaea1fd 100644 --- a/src/igraph/test/foreign.py +++ b/src/igraph/test/foreign.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import io import unittest import warnings @@ -22,7 +20,8 @@ class ForeignTests(unittest.TestCase): def testDIMACS(self): - with temporary_file(u"""\ + with temporary_file( + """\ c c This is a simple example file to demonstrate the c DIMACS input file format for minimum-cost flow problems. @@ -40,17 +39,18 @@ def testDIMACS(self): a 2 3 2 a 2 4 3 a 3 4 5 - """) as tmpfname: + """ + ) as tmpfname: graph = Graph.Read_DIMACS(tmpfname, False) self.assertTrue(isinstance(graph, Graph)) self.assertTrue(graph.vcount() == 4 and graph.ecount() == 5) self.assertTrue(graph["source"] == 0 and graph["target"] == 3) - self.assertTrue(graph.es["capacity"] == [4,2,2,3,5]) + self.assertTrue(graph.es["capacity"] == [4, 2, 2, 3, 5]) graph.write_dimacs(tmpfname) - def testDL(self): - with temporary_file(u"""\ + with temporary_file( + """\ dl n=5 format = fullmatrix labels embedded @@ -61,16 +61,32 @@ def testDL(self): Lin 1 0 0 1 0 Pat 1 0 1 0 1 russ 0 1 0 1 0 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_DL(tmpfname) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 5 and g.ecount() == 12) self.assertTrue(g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(0,3),(1,0),(1,4),(2,0),(2,3),(3,0),\ - (3,2),(3,4),(4,1),(4,3)]) - - with temporary_file(u"""\ + self.assertTrue( + sorted(g.get_edgelist()) + == [ + (0, 1), + (0, 2), + (0, 3), + (1, 0), + (1, 4), + (2, 0), + (2, 3), + (3, 0), + (3, 2), + (3, 4), + (4, 1), + (4, 3), + ] + ) + + with temporary_file( + """\ dl n=5 format = fullmatrix labels: @@ -83,16 +99,32 @@ def testDL(self): 1 0 0 1 0 1 0 1 0 1 0 1 0 1 0 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_DL(tmpfname) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 5 and g.ecount() == 12) self.assertTrue(g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(0,3),(1,0),(1,4),(2,0),(2,3),(3,0),\ - (3,2),(3,4),(4,1),(4,3)]) - - with temporary_file(u"""\ + self.assertTrue( + sorted(g.get_edgelist()) + == [ + (0, 1), + (0, 2), + (0, 3), + (1, 0), + (1, 4), + (2, 0), + (2, 3), + (3, 0), + (3, 2), + (3, 4), + (4, 1), + (4, 3), + ] + ) + + with temporary_file( + """\ DL n=5 format = edgelist1 labels: @@ -104,60 +136,70 @@ def testDL(self): sally jim 4 billy george 5 jane jim 6 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_DL(tmpfname, False) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 5 and g.ecount() == 5) self.assertTrue(not g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(0,3),(1,2),(2,4)]) + self.assertTrue( + sorted(g.get_edgelist()) == [(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)] + ) def _testNCOLOrLGL(self, func, fname, can_be_reopened=True): - g = func(fname, names=False, weights=False, \ - directed=False) + g = func(fname, names=False, weights=False, directed=False) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 4 and g.ecount() == 5) self.assertTrue(not g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(1,1),(1,3),(2,3)]) - self.assertTrue("name" not in g.vertex_attributes() and \ - "weight" not in g.edge_attributes()) + self.assertTrue( + sorted(g.get_edgelist()) == [(0, 1), (0, 2), (1, 1), (1, 3), (2, 3)] + ) + self.assertTrue( + "name" not in g.vertex_attributes() and "weight" not in g.edge_attributes() + ) if not can_be_reopened: return - g = func(fname, names=False, \ - directed=False) - self.assertTrue("name" not in g.vertex_attributes() and \ - "weight" in g.edge_attributes()) + g = func(fname, names=False, directed=False) + self.assertTrue( + "name" not in g.vertex_attributes() and "weight" in g.edge_attributes() + ) self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) g = func(fname, directed=False) - self.assertTrue("name" in g.vertex_attributes() and \ - "weight" in g.edge_attributes()) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" in g.edge_attributes() + ) self.assertTrue(g.vs["name"] == ["eggs", "spam", "ham", "bacon"]) self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) def testNCOL(self): - with temporary_file(u"""\ + with temporary_file( + """\ eggs spam 1 ham eggs 2 ham bacon bacon spam 3 - spam spam""") as tmpfname: + spam spam""" + ) as tmpfname: self._testNCOLOrLGL(func=Graph.Read_Ncol, fname=tmpfname) - with temporary_file(u"""\ + with temporary_file( + """\ eggs spam ham eggs ham bacon bacon spam - spam spam""") as tmpfname: + spam spam""" + ) as tmpfname: g = Graph.Read_Ncol(tmpfname) - self.assertTrue("name" in g.vertex_attributes() and \ - "weight" not in g.edge_attributes()) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() + ) def testLGL(self): - with temporary_file(u"""\ + with temporary_file( + """\ # eggs spam 1 # ham @@ -166,10 +208,12 @@ def testLGL(self): # bacon spam 3 # spam - spam""") as tmpfname: + spam""" + ) as tmpfname: self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=tmpfname) - with temporary_file(u"""\ + with temporary_file( + """\ # eggs spam # ham @@ -178,23 +222,28 @@ def testLGL(self): # bacon spam # spam - spam""") as tmpfname: + spam""" + ) as tmpfname: with warnings.catch_warnings(): warnings.simplefilter("ignore") g = Graph.Read_Lgl(tmpfname) - self.assertTrue("name" in g.vertex_attributes() and \ - "weight" not in g.edge_attributes()) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() + ) # This is not an LGL file; we are testing error handling here - with temporary_file(u"""\ + with temporary_file( + """\ 1 2 1 3 - """) as tmpfname: + """ + ) as tmpfname: with self.assertRaises(InternalError): Graph.Read_Lgl(tmpfname) def testLGLWithIOModule(self): - with temporary_file(u"""\ + with temporary_file( + """\ # eggs spam 1 # ham @@ -203,13 +252,16 @@ def testLGLWithIOModule(self): # bacon spam 3 # spam - spam""") as tmpfname: + spam""" + ) as tmpfname: with io.open(tmpfname, "r") as fp: - self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=fp, - can_be_reopened=False) + self._testNCOLOrLGL( + func=Graph.Read_Lgl, fname=fp, can_be_reopened=False + ) def testAdjacency(self): - with temporary_file(u"""\ + with temporary_file( + """\ # Test comment line 0 1 1 0 0 0 1 0 1 0 0 0 @@ -217,22 +269,73 @@ def testAdjacency(self): 0 0 0 0 2 2 0 0 0 2 0 2 0 0 0 2 2 0 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_Adjacency(tmpfname) self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 6 and g.ecount() == 18 and - g.is_directed() and "weight" not in g.edge_attributes()) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 18 + and g.is_directed() + and "weight" not in g.edge_attributes() + ) g = Graph.Read_Adjacency(tmpfname, attribute="weight") self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 6 and g.ecount() == 12 and - g.is_directed() and g.es["weight"] == [1,1,1,1,1,1,2,2,2,2,2,2]) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 12 + and g.is_directed() + and g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2] + ) g.write_adjacency(tmpfname) def testPickle(self): - pickle = [128, 2, 99, 105, 103, 114, 97, 112, 104, 10, 71, 114, 97, 112, - 104, 10, 113, 1, 40, 75, 3, 93, 113, 2, 75, 1, 75, 2, 134, 113, 3, 97, - 137, 125, 125, 125, 116, 82, 113, 4, 125, 98, 46] + pickle = [ + 128, + 2, + 99, + 105, + 103, + 114, + 97, + 112, + 104, + 10, + 71, + 114, + 97, + 112, + 104, + 10, + 113, + 1, + 40, + 75, + 3, + 93, + 113, + 2, + 75, + 1, + 75, + 2, + 134, + 113, + 3, + 97, + 137, + 125, + 125, + 125, + 116, + 82, + 113, + 4, + 125, + 98, + 46, + ] if sys.version_info > (3, 0): pickle = bytes(pickle) else: @@ -240,17 +343,16 @@ def testPickle(self): with temporary_file(pickle, "wb") as tmpfname: g = Graph.Read_Pickle(pickle) self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and - not g.is_directed()) + self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) @unittest.skipIf(nx is None, "test case depends on networkx") def testGraphNetworkx(self): # Undirected g = Graph.Ring(10) - g['gattr'] = 'graph_attribute' - g.vs['vattr'] = list(range(g.vcount())) - g.es['eattr'] = list(range(len(g.es))) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) # Go to networkx and back g_nx = g.to_networkx() @@ -263,16 +365,13 @@ def testGraphNetworkx(self): # Test attributes self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual( - sorted(['vattr', '_nx_name']), - sorted(g2.vertex_attributes())) + self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) for i, vertex in enumerate(g.vs): vertex2 = g2.vs[i] for an in vertex.attribute_names(): - if an == 'vattr': + if an == "vattr": continue - self.assertEqual( - vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) self.assertEqual(g.edge_attributes(), g2.edge_attributes()) for edge in g.es: eid = g2.get_eid(edge.source, edge.target) @@ -297,9 +396,9 @@ def testMultigraphNetworkx(self): # Undirected g = Graph.Ring(10) g.add_edge(0, 1) - g['gattr'] = 'graph_attribute' - g.vs['vattr'] = list(range(g.vcount())) - g.es['eattr'] = list(range(len(g.es))) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) # Go to networkx and back g_nx = g.to_networkx() @@ -312,9 +411,7 @@ def testMultigraphNetworkx(self): # Test attributes self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual( - sorted(['vattr', '_nx_name']), - sorted(g2.vertex_attributes())) + self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) self.assertEqual(g.edge_attributes(), g2.edge_attributes()) # Testing parallel edges is a bit more tricky edge2_found = set() @@ -356,15 +453,16 @@ def testMultigraphNetworkx(self): def testGraphGraphTool(self): # Undirected g = Graph.Ring(10) - g['gattr'] = 'graph_attribute' - g.vs['vattr'] = list(range(g.vcount())) - g.es['eattr'] = list(range(len(g.es))) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) # Go to graph-tool and back g_gt = g.to_graph_tool( - graph_attributes={'gattr': 'object'}, - vertex_attributes={'vattr': 'int'}, - edge_attributes={'eattr': 'int'}) + graph_attributes={"gattr": "object"}, + vertex_attributes={"vattr": "int"}, + edge_attributes={"eattr": "int"}, + ) g2 = Graph.from_graph_tool(g_gt) self.assertFalse(g2.is_directed()) @@ -378,8 +476,7 @@ def testGraphGraphTool(self): for i, vertex in enumerate(g.vs): vertex2 = g2.vs[i] for an in vertex.attribute_names(): - self.assertEqual( - vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) self.assertEqual(g.edge_attributes(), g2.edge_attributes()) for edge in g.es: eid = g2.get_eid(edge.source, edge.target) @@ -405,15 +502,16 @@ def testMultigraphGraphTool(self): # Undirected g = Graph.Ring(10) g.add_edge(0, 1) - g['gattr'] = 'graph_attribute' - g.vs['vattr'] = list(range(g.vcount())) - g.es['eattr'] = list(range(len(g.es))) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) # Go to graph-tool and back g_gt = g.to_graph_tool( - graph_attributes={'gattr': 'object'}, - vertex_attributes={'vattr': 'int'}, - edge_attributes={'eattr': 'int'}) + graph_attributes={"gattr": "object"}, + vertex_attributes={"vattr": "int"}, + edge_attributes={"eattr": "int"}, + ) g2 = Graph.from_graph_tool(g_gt) self.assertFalse(g2.is_directed()) @@ -427,8 +525,7 @@ def testMultigraphGraphTool(self): for i, vertex in enumerate(g.vs): vertex2 = g2.vs[i] for an in vertex.attribute_names(): - self.assertEqual( - vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) self.assertEqual(g.edge_attributes(), g2.edge_attributes()) # Testing parallel edges is a bit more tricky edge2_found = set() @@ -471,10 +568,11 @@ def suite(): foreign_suite = unittest.makeSuite(ForeignTests) return unittest.TestSuite([foreign_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/src/igraph/utils.py b/src/igraph/utils.py index ea88a81d9..b60eb666e 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -6,6 +6,7 @@ """ from contextlib import contextmanager + try: from collections.abc import MutableMapping except ImportError: @@ -17,13 +18,17 @@ import tempfile __all__ = ( - "dbl_epsilon", "multidict", "named_temporary_file", - "numpy_to_contiguous_memoryview", "rescale", - "safemin", "safemax" + "dbl_epsilon", + "multidict", + "named_temporary_file", + "numpy_to_contiguous_memoryview", + "rescale", + "safemin", + "safemax", ) __docformat__ = "restructuredtext en" -__license__ = u"""\ +__license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz Pázmány Péter sétány 1/a, 1117 Budapest, Hungary @@ -49,6 +54,7 @@ def _is_running_in_ipython(): IPython or not.""" try: from IPython import get_ipython + return get_ipython() is not None except ImportError: return False @@ -91,8 +97,7 @@ def numpy_to_contiguous_memoryview(obj): return memoryview(require(obj, dtype=dtype, requirements="AC")) -def rescale(values, out_range=(0., 1.), in_range=None, clamp=False, - scale=None): +def rescale(values, out_range=(0.0, 1.0), in_range=None, clamp=False, scale=None): """Rescales a list of numbers into a given range. `out_range` gives the range of the output values; by default, the minimum @@ -148,9 +153,9 @@ def rescale(values, out_range=(0., 1.), in_range=None, clamp=False, ratio = float(ma - mi) if not ratio: - return [(out_range[0] + out_range[1]) / 2.] * len(values) + return [(out_range[0] + out_range[1]) / 2.0] * len(values) - min_out, max_out = map(float, out_range) + min_out, max_out = list(map(float, out_range)) ratio = (max_out - min_out) / ratio result = [(x - mi) * ratio + min_out for x in values] @@ -160,9 +165,7 @@ def rescale(values, out_range=(0., 1.), in_range=None, clamp=False, return result -def str_to_orientation( - value, reversed_horizontal=False, reversed_vertical=False -): +def str_to_orientation(value, reversed_horizontal=False, reversed_vertical=False): """Tries to interpret a string as an orientation value. The following basic values are understood: ``left-right``, ``bottom-top``, @@ -186,9 +189,16 @@ def str_to_orientation( """ aliases = { - "left-right": "lr", "right-left": "rl", "top-bottom": "tb", - "bottom-top": "bt", "top-down": "tb", "bottom-up": "bt", - "top-bottom": "tb", "bottom-top": "bt", "td": "tb", "bu": "bt" + "left-right": "lr", + "right-left": "rl", + "top-bottom": "tb", + "bottom-top": "bt", + "top-down": "tb", + "bottom-up": "bt", + "top-bottom": "tb", + "bottom-top": "bt", + "td": "tb", + "bu": "bt", } dir = ["lr", "rl"][reversed_horizontal] @@ -227,7 +237,7 @@ def consecutive_pairs(iterable, circular=False): it = iter(iterable) try: - prev = it.next() + prev = next(it) except StopIteration: return first = prev @@ -257,8 +267,8 @@ def __init__(self, *args, **kwds): self._dict = {} if len(args) > 1: raise ValueError( - "%r expected at most 1 argument, got %d" % - (self.__class__.__name__, len(args)) + "%r expected at most 1 argument, got %d" + % (self.__class__.__name__, len(args)) ) if args: args = args[0] @@ -356,21 +366,21 @@ def getlist(self, key): def iterlists(self): """Iterates over ``(key, values)`` pairs where ``values`` is the list of values associated with ``key``.""" - return self._dict.iteritems() + return iter(self._dict.items()) def lists(self): """Returns a list of ``(key, values)`` pairs where ``values`` is the list of values associated with ``key``.""" - return self._dict.items() + return list(self._dict.items()) def update(self, arg, **kwds): if hasattr(arg, "keys") and callable(arg.keys): - for key in arg.keys(): + for key in list(arg.keys()): self.add(key, arg[key]) else: for key, value in arg: self.add(key, value) - for key, value in kwds.iteritems(): + for key, value in kwds.items(): self.add(key, value) @@ -389,7 +399,7 @@ def safemax(iterable, default=0): """ it = iter(iterable) try: - first = it.next() + first = next(it) except StopIteration: return default else: @@ -411,7 +421,7 @@ def safemin(iterable, default=0): """ it = iter(iterable) try: - first = it.next() + first = next(it) except StopIteration: return default else: diff --git a/tests/test_structural.py b/tests/test_structural.py index 2f192ce4c..e3e72d516 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -5,7 +5,7 @@ import warnings from igraph import * -from igraph.compat import isnan +from math import isnan class SimplePropertiesTests(unittest.TestCase): gfull = Graph.Full(10) diff --git a/tox.ini b/tox.ini index 055ea15f8..e70312f9f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py36, py37, py38, py39, pypy, pypy3 +envlist = py36, py37, py38, py39, pypy, pypy3 [testenv] commands = python -m unittest @@ -16,9 +16,6 @@ deps = setenv = TESTING_IN_TOX=1 -[testenv:py27] -commands = python -m unittest discover - [testenv:py39] # py39 support is still sparse; most of the optional dependencies have no # wheels for Python 3.9 yet so we install only those where they do From 705da0a18f46db551cd2832037c24c01296b25fb Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 15 Jan 2021 21:18:24 +1100 Subject: [PATCH 0089/1681] 2to3 and black lint of tests --- tests/test_atlas.py | 109 +++++-- tests/test_attributes.py | 47 +-- tests/test_basic.py | 256 +++++++-------- tests/test_bipartite.py | 146 ++++++--- tests/test_cliques.py | 191 +++++++---- tests/test_colortests.py | 83 ++--- tests/test_conversion.py | 154 +++++---- tests/test_decomposition.py | 382 +++++++++++++++------- tests/test_edgeseq.py | 8 +- tests/test_flow.py | 96 +++--- tests/test_foreign.py | 227 +++++++++---- tests/test_games.py | 71 +++-- tests/test_generators.py | 161 +++++----- tests/test_homepage.py | 19 +- tests/test_indexing.py | 13 +- tests/test_isomorphism.py | 340 +++++++++++++------- tests/test_iterators.py | 33 +- tests/test_layouts.py | 141 ++++---- tests/test_matching.py | 56 +++- tests/test_operators.py | 345 ++++++++++++++------ tests/test_rng.py | 9 +- tests/test_separators.py | 20 +- tests/test_spectral.py | 59 ++-- tests/test_structural.py | 600 ++++++++++++++++++++++++----------- tests/test_unicode_issues.py | 12 +- tests/test_vertexseq.py | 8 +- tests/test_walks.py | 6 +- tests/utils.py | 10 +- 28 files changed, 2346 insertions(+), 1256 deletions(-) diff --git a/tests/test_atlas.py b/tests/test_atlas.py index 438c2a04a..322a61f89 100644 --- a/tests/test_atlas.py +++ b/tests/test_atlas.py @@ -1,5 +1,3 @@ -from __future__ import division - import warnings import unittest from igraph import * @@ -11,17 +9,27 @@ def testPageRank(self): try: pr = g.pagerank() except Exception as ex: - self.assertTrue(False, msg="PageRank calculation threw exception for graph #%d: %s" % (idx, ex)) + self.assertTrue( + False, + msg="PageRank calculation threw exception for graph #%d: %s" + % (idx, ex), + ) raise if g.vcount() == 0: self.assertEqual([], pr) continue - self.assertAlmostEqual(1.0, sum(pr), places=5, \ - msg="PageRank sum is not 1.0 for graph #%d (%r)" % (idx, pr)) - self.assertTrue(min(pr) >= 0, \ - msg="Minimum PageRank is less than 0 for graph #%d (%r)" % (idx, pr)) + self.assertAlmostEqual( + 1.0, + sum(pr), + places=5, + msg="PageRank sum is not 1.0 for graph #%d (%r)" % (idx, pr), + ) + self.assertTrue( + min(pr) >= 0, + msg="Minimum PageRank is less than 0 for graph #%d (%r)" % (idx, pr), + ) def testEigenvectorCentrality(self): # Temporarily turn off the warning handler because g.evcent() will print @@ -37,7 +45,11 @@ def testEigenvectorCentrality(self): try: ec, eval = g.evcent(return_eigenvalue=True) except Exception as ex: - self.assertTrue(False, msg="Eigenvector centrality threw exception for graph #%d: %s" % (idx, ex)) + self.assertTrue( + False, + msg="Eigenvector centrality threw exception for graph #%d: %s" + % (idx, ex), + ) raise if g.vcount() == 0: @@ -50,23 +62,40 @@ def testEigenvectorCentrality(self): n = g.vcount() if abs(eval) < 1e-4: - self.assertTrue(min(ec) >= -1e-10, - msg="Minimum eigenvector centrality is smaller than 0 for graph #%d" % idx) - self.assertTrue(max(ec) <= 1, - msg="Maximum eigenvector centrality is greater than 1 for graph #%d" % idx) + self.assertTrue( + min(ec) >= -1e-10, + msg="Minimum eigenvector centrality is smaller than 0 for graph #%d" + % idx, + ) + self.assertTrue( + max(ec) <= 1, + msg="Maximum eigenvector centrality is greater than 1 for graph #%d" + % idx, + ) continue - self.assertAlmostEqual(max(ec), 1, places=7, \ - msg="Maximum eigenvector centrality is %r (not 1) for graph #%d (%r)" % \ - (max(ec), idx, ec)) - self.assertTrue(min(ec) >= 0, \ - msg="Minimum eigenvector centrality is less than 0 for graph #%d" % idx) + self.assertAlmostEqual( + max(ec), + 1, + places=7, + msg="Maximum eigenvector centrality is %r (not 1) for graph #%d (%r)" + % (max(ec), idx, ec), + ) + self.assertTrue( + min(ec) >= 0, + msg="Minimum eigenvector centrality is less than 0 for graph #%d" + % idx, + ) ec2 = [sum(ec[u.index] for u in v.predecessors()) for v in g.vs] for i in range(n): - self.assertAlmostEqual(ec[i] * eval, ec2[i], places=7, \ - msg="Eigenvector centrality in graph #%d seems to be invalid "\ - "for vertex %d" % (idx, i)) + self.assertAlmostEqual( + ec[i] * eval, + ec2[i], + places=7, + msg="Eigenvector centrality in graph #%d seems to be invalid " + "for vertex %d" % (idx, i), + ) finally: # Reset the warning handler warnings.resetwarnings() @@ -78,10 +107,15 @@ def testHubScore(self): self.assertEqual([], sc) continue - self.assertAlmostEqual(max(sc), 1, places=7, \ - msg="Maximum authority score is not 1 for graph #%d" % idx) - self.assertTrue(min(sc) >= 0, \ - msg="Minimum hub score is less than 0 for graph #%d" % idx) + self.assertAlmostEqual( + max(sc), + 1, + places=7, + msg="Maximum authority score is not 1 for graph #%d" % idx, + ) + self.assertTrue( + min(sc) >= 0, msg="Minimum hub score is less than 0 for graph #%d" % idx + ) def testAuthorityScore(self): for idx, g in enumerate(self.__class__.graphs): @@ -90,29 +124,40 @@ def testAuthorityScore(self): self.assertEqual([], sc) continue - self.assertAlmostEqual(max(sc), 1, places=7, \ - msg="Maximum authority score is not 1 for graph #%d" % idx) - self.assertTrue(min(sc) >= 0, \ - msg="Minimum authority score is less than 0 for graph #%d" % idx) + self.assertAlmostEqual( + max(sc), + 1, + places=7, + msg="Maximum authority score is not 1 for graph #%d" % idx, + ) + self.assertTrue( + min(sc) >= 0, + msg="Minimum authority score is less than 0 for graph #%d" % idx, + ) + class GraphAtlasTests(unittest.TestCase, AtlasTestBase): graphs = [Graph.Atlas(i) for i in range(1253)] skip_graphs = set([180]) + class IsoclassTests(unittest.TestCase, AtlasTestBase): - graphs = [Graph.Isoclass(3, i, directed=True) for i in range(16)] + \ - [Graph.Isoclass(4, i, directed=True) for i in range(218)] + graphs = [Graph.Isoclass(3, i, directed=True) for i in range(16)] + [ + Graph.Isoclass(4, i, directed=True) for i in range(218) + ] skip_graphs = set([136]) + def suite(): atlas_suite = unittest.makeSuite(GraphAtlasTests) isoclass_suite = unittest.makeSuite(IsoclassTests) return unittest.TestSuite([atlas_suite, isoclass_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 4d81ee18f..a95c49261 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -3,6 +3,7 @@ import unittest from igraph import * + class AttributeTests(unittest.TestCase): def testGraphAttributes(self): g = Graph.Full(5) @@ -45,23 +46,23 @@ def testMassVertexAttributeAssignment(self): g = Graph.Full(5) g.vs.set_attribute_values("name", list(range(5))) self.assertTrue(g.vs.get_attribute_values("name") == list(range(5))) - g.vs["name"] = list(range(5,10)) - self.assertTrue(g.vs["name"] == list(range(5,10))) - g.vs["name2"] = (1,2,3,4,6) - self.assertTrue(g.vs["name2"] == [1,2,3,4,6]) + g.vs["name"] = list(range(5, 10)) + self.assertTrue(g.vs["name"] == list(range(5, 10))) + g.vs["name2"] = (1, 2, 3, 4, 6) + self.assertTrue(g.vs["name2"] == [1, 2, 3, 4, 6]) g.vs.set_attribute_values("name", [2]) - self.assertTrue(g.vs["name"] == [2]*5) + self.assertTrue(g.vs["name"] == [2] * 5) def testMassEdgeAttributeAssignment(self): g = Graph.Full(5) g.es.set_attribute_values("name", list(range(10))) self.assertTrue(g.es.get_attribute_values("name") == list(range(10))) - g.es["name"] = list(range(10,20)) - self.assertTrue(g.es["name"] == list(range(10,20))) - g.es["name2"] = (1,2,3,4,6,1,2,3,4,6) - self.assertTrue(g.es["name2"] == [1,2,3,4,6,1,2,3,4,6]) + g.es["name"] = list(range(10, 20)) + self.assertTrue(g.es["name"] == list(range(10, 20))) + g.es["name2"] = (1, 2, 3, 4, 6, 1, 2, 3, 4, 6) + self.assertTrue(g.es["name2"] == [1, 2, 3, 4, 6, 1, 2, 3, 4, 6]) g.es.set_attribute_values("name", [2]) - self.assertTrue(g.es["name"] == [2]*10) + self.assertTrue(g.es["name"] == [2] * 10) def testVertexNameIndexing(self): g = Graph.Famous("bull") @@ -71,10 +72,10 @@ def testVertexNameIndexing(self): g.vs[2]["name"] = "quack" self.assertRaises(ValueError, g.degree, "baz") self.assertTrue(g.degree("quack") == 3) - self.assertTrue(g.degree(u"quack") == 3) - self.assertTrue(g.degree([u"bar", u"thud", 0]) == [3, 1, 2]) + self.assertTrue(g.degree("quack") == 3) + self.assertTrue(g.degree(["bar", "thud", 0]) == [3, 1, 2]) del g.vs["name"] - self.assertRaises(ValueError, g.degree, [u"bar", u"thud", 0]) + self.assertRaises(ValueError, g.degree, ["bar", "thud", 0]) def testVertexNameIndexingBytes(self): g = Graph.Famous("bull") @@ -91,7 +92,7 @@ def testUnhashableVertexNames(self): g = Graph.Famous("bull") g.vs["name"] = [str(x) for x in range(4)] - value = "this is not hashable".split() + value = "this is not hashable".split() g.vs[2]["name"] = value # Trigger an indexing by doing a lookup by name @@ -108,7 +109,7 @@ def testUnhashableVertexNames(self): def testVertexNameIndexingBug196(self): g = Graph() - a, b = b'a', b'b' + a, b = b"a", b"b" g.add_vertices([a, b]) g.add_edges([(a, b)]) self.assertEqual(g.ecount(), 1) @@ -126,9 +127,10 @@ def testInvalidAttributeNames(self): self.assertRaises(TypeError, g.es[0].__setitem__, attr_name, "foo") self.assertRaises(TypeError, g.es[0].__getitem__, attr_name, "foo") + class AttributeCombinationTests(unittest.TestCase): def setUp(self): - el = [(0,1), (1,0), (1,2), (2,3), (2,3), (2,3), (3,3)] + el = [(0, 1), (1, 0), (1, 2), (2, 3), (2, 3), (2, 3), (3, 3)] self.g = Graph(el) self.g.es["weight"] = [1, 2, 3, 4, 5, 6, 7] self.g.es["weight2"] = [1, 2, 3, 4, 5, 6, 7] @@ -267,20 +269,23 @@ def testCombinationNone(self): class UnicodeAttributeTests(unittest.TestCase): def testUnicodeAttributeNameCombination(self): g = Graph.Erdos_Renyi(n=9, m=20) - g.es[u"test"] = 1 - g.contract_vertices([0,0,0,1,1,1,2,2,2]) + g.es["test"] = 1 + g.contract_vertices([0, 0, 0, 1, 1, 1, 2, 2, 2]) def suite(): attribute_suite = unittest.makeSuite(AttributeTests) attribute_combination_suite = unittest.makeSuite(AttributeCombinationTests) unicode_attributes_suite = unittest.makeSuite(UnicodeAttributeTests) - return unittest.TestSuite([attribute_suite, attribute_combination_suite, - unicode_attributes_suite]) + return unittest.TestSuite( + [attribute_suite, attribute_combination_suite, unicode_attributes_suite] + ) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() diff --git a/tests/test_basic.py b/tests/test_basic.py index 5368db09e..2d7c7be6a 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,8 +1,13 @@ import unittest from igraph import ( - ALL, Graph, IN, InternalError, is_degree_sequence, - is_graphical_degree_sequence, Matrix + ALL, + Graph, + IN, + InternalError, + is_degree_sequence, + is_graphical_degree_sequence, + Matrix, ) try: @@ -15,32 +20,35 @@ class BasicTests(unittest.TestCase): def testGraphCreation(self): g = Graph() self.assertTrue(isinstance(g, Graph)) - self.assertTrue( - g.vcount() == 0 and g.ecount() == 0 and not g.is_directed() - ) + self.assertTrue(g.vcount() == 0 and g.ecount() == 0 and not g.is_directed()) g = Graph(3, [(0, 1), (1, 2), (2, 0)]) self.assertTrue( - g.vcount() == 3 and g.ecount() == 3 and not g.is_directed() and - g.is_simple() + g.vcount() == 3 + and g.ecount() == 3 + and not g.is_directed() + and g.is_simple() ) g = Graph(2, [(0, 1), (1, 2), (2, 3)], True) self.assertTrue( - g.vcount() == 4 and g.ecount() == 3 and g.is_directed() and - g.is_simple() + g.vcount() == 4 and g.ecount() == 3 and g.is_directed() and g.is_simple() ) g = Graph([(0, 1), (1, 2), (2, 1)]) self.assertTrue( - g.vcount() == 3 and g.ecount() == 3 and not g.is_directed() and - not g.is_simple() + g.vcount() == 3 + and g.ecount() == 3 + and not g.is_directed() + and not g.is_simple() ) g = Graph(((0, 1), (0, 0), (1, 2))) self.assertTrue( - g.vcount() == 3 and g.ecount() == 3 and not g.is_directed() and - not g.is_simple() + g.vcount() == 3 + and g.ecount() == 3 + and not g.is_directed() + and not g.is_simple() ) g = Graph(8, None) @@ -61,8 +69,7 @@ def testGraphCreationWithNumPy(self): arr = np.array([(0, 1), (1, 2), (2, 3)]) g = Graph(arr, directed=True) self.assertTrue( - g.vcount() == 4 and g.ecount() == 3 and g.is_directed() and - g.is_simple() + g.vcount() == 4 and g.ecount() == 3 and g.is_directed() and g.is_simple() ) # Sliced NumPy array -- the sliced array is non-contiguous but we @@ -70,8 +77,7 @@ def testGraphCreationWithNumPy(self): arr = np.array([(0, 1), (10, 11), (1, 2), (11, 12), (2, 3), (12, 13)]) g = Graph(arr[::2, :], directed=True) self.assertTrue( - g.vcount() == 4 and g.ecount() == 3 and g.is_directed() and - g.is_simple() + g.vcount() == 4 and g.ecount() == 3 and g.is_directed() and g.is_simple() ) # 1D NumPy array -- should raise a TypeError because we need a 2D array @@ -79,9 +85,7 @@ def testGraphCreationWithNumPy(self): self.assertRaises(TypeError, Graph, arr) # 3D NumPy array -- should raise a TypeError because we need a 2D array - arr = np.array( - [([0, 1], [10, 11]), ([1, 2], [11, 12]), ([2, 3], [12, 13])] - ) + arr = np.array([([0, 1], [10, 11]), ([1, 2], [11, 12]), ([2, 3], [12, 13])]) self.assertRaises(TypeError, Graph, arr) # NumPy array with strings -- should be a casting error @@ -117,9 +121,7 @@ def testAddVertex(self): vertex = g.add_vertex(name="frob", spam="cheese", ham=42) self.assertTrue(g.vcount() == 5 and g.ecount() == 0) self.assertEqual(4, vertex.index) - self.assertEqual( - sorted(g.vertex_attributes()), ["ham", "name", "spam"] - ) + self.assertEqual(sorted(g.vertex_attributes()), ["ham", "name", "spam"]) self.assertEqual(g.vs["spam"], [None] * 4 + ["cheese"]) self.assertEqual(g.vs["ham"], [None] * 4 + [42]) @@ -136,7 +138,7 @@ def testAddVertices(self): self.assertTrue(g.vcount() == 5 and g.ecount() == 0) self.assertEqual(g.vs[2:]["name"], ["spam", "bacon", "eggs"]) - g.add_vertices(2, attributes={'color': ['k', 'b']}) + g.add_vertices(2, attributes={"color": ["k", "b"]}) self.assertEqual(g.vs[2:]["name"], ["spam", "bacon", "eggs", None, None]) self.assertEqual(g.vs[5:]["color"], ["k", "b"]) @@ -214,17 +216,25 @@ def testAddEdges(self): g.add_edges([("spam", "eggs"), ("spam", "ham")]) self.assertEqual(g.vcount(), 4) - self.assertEqual(g.get_edgelist(), [ - (0, 1), (1, 2), (2, 3), (1, 3), (0, 2), (0, 3) - ]) - - g.add_edges([(0, 0), (1, 1)], attributes={'color': ['k', 'b']}) - self.assertEqual(g.get_edgelist(), [ - (0, 1), (1, 2), (2, 3), (1, 3), (0, 2), (0, 3), (0, 0), (1, 1), - ]) self.assertEqual( - g.es['color'], - [None, None, None, None, None, None, 'k', 'b']) + g.get_edgelist(), [(0, 1), (1, 2), (2, 3), (1, 3), (0, 2), (0, 3)] + ) + + g.add_edges([(0, 0), (1, 1)], attributes={"color": ["k", "b"]}) + self.assertEqual( + g.get_edgelist(), + [ + (0, 1), + (1, 2), + (2, 3), + (1, 3), + (0, 2), + (0, 3), + (0, 0), + (1, 1), + ], + ) + self.assertEqual(g.es["color"], [None, None, None, None, None, None, "k", "b"]) def testDeleteEdges(self): g = Graph.Famous("petersen") @@ -297,7 +307,7 @@ def testGraphGetEid(self): g = Graph.Famous("petersen") g.vs["name"] = list("ABCDEFGHIJ") edges_to_ids = dict((v, k) for k, v in enumerate(g.get_edgelist())) - for (source, target), edge_id in edges_to_ids.items(): + for (source, target), edge_id in list(edges_to_ids.items()): source_name, target_name = g.vs[(source, target)]["name"] self.assertEqual(edge_id, g.get_eid(source, target)) self.assertEqual(edge_id, g.get_eid(source_name, target_name)) @@ -343,28 +353,38 @@ def testMultiplesLoops(self): g.add_edges([(0, 1), (7, 7), (6, 6), (6, 6), (6, 6)]) # is_loop - self.assertTrue(g.is_loop() == [ - False, False, False, False, False, False, False, - True, True, True, True - ]) - self.assertTrue(g.is_loop(g.ecount()-2)) - self.assertTrue(g.is_loop(range(6, 8)) == [False, True]) + self.assertTrue( + g.is_loop() + == [False, False, False, False, False, False, False, True, True, True, True] + ) + self.assertTrue(g.is_loop(g.ecount() - 2)) + self.assertTrue(g.is_loop(list(range(6, 8))) == [False, True]) # is_multiple - self.assertTrue(g.is_multiple() == [ - False, False, False, False, False, False, True, - False, False, True, True - ]) + self.assertTrue( + g.is_multiple() + == [ + False, + False, + False, + False, + False, + False, + True, + False, + False, + True, + True, + ] + ) # has_multiple self.assertTrue(g.has_multiple()) # count_multiple - self.assertTrue( - g.count_multiple() == [2, 1, 1, 1, 1, 1, 2, 1, 3, 3, 3] - ) - self.assertTrue(g.count_multiple(g.ecount()-1) == 3) - self.assertTrue(g.count_multiple(range(2, 5)) == [1, 1, 1]) + self.assertTrue(g.count_multiple() == [2, 1, 1, 1, 1, 1, 2, 1, 3, 3, 3]) + self.assertTrue(g.count_multiple(g.ecount() - 1) == 3) + self.assertTrue(g.count_multiple(list(range(2, 5))) == [1, 1, 1]) # check if a mutual directed edge pair is reported as multiple g = Graph(2, [(0, 1), (1, 0)], directed=True) @@ -372,6 +392,7 @@ def testMultiplesLoops(self): def testPickling(self): import pickle + g = Graph([(0, 1), (1, 2)]) g["data"] = "abcdef" g.vs["data"] = [3, 4, 5] @@ -448,22 +469,16 @@ class GraphDictListTests(unittest.TestCase): def setUp(self): self.vertices = [ {"name": "Alice", "age": 48, "gender": "F"}, - {"name": "Bob", "age": 33, "gender": "M"}, + {"name": "Bob", "age": 33, "gender": "M"}, {"name": "Cecil", "age": 45, "gender": "F"}, - {"name": "David", "age": 34, "gender": "M"} + {"name": "David", "age": 34, "gender": "M"}, ] self.edges = [ {"source": "Alice", "target": "Bob", "friendship": 4, "advice": 4}, {"source": "Cecil", "target": "Bob", "friendship": 5, "advice": 5}, - { - "source": "Cecil", "target": "Alice", - "friendship": 5, "advice": 5 - }, - { - "source": "David", "target": "Alice", - "friendship": 2, "advice": 4 - }, - {"source": "David", "target": "Bob", "friendship": 1, "advice": 2} + {"source": "Cecil", "target": "Alice", "friendship": 5, "advice": 5}, + {"source": "David", "target": "Alice", "friendship": 2, "advice": 4}, + {"source": "David", "target": "Bob", "friendship": 1, "advice": 2}, ] def testGraphFromDictList(self): @@ -475,9 +490,7 @@ def testGraphFromDictList(self): def testGraphFromDictIterator(self): g = Graph.DictList(iter(self.vertices), iter(self.edges)) self.checkIfOK(g, "name") - g = Graph.DictList( - iter(self.vertices), iter(self.edges), iterative=True - ) + g = Graph.DictList(iter(self.vertices), iter(self.edges), iterative=True) self.checkIfOK(g, "name") def testGraphFromDictIteratorNoVertices(self): @@ -487,19 +500,15 @@ def testGraphFromDictIteratorNoVertices(self): self.checkIfOK(g, "name", check_vertex_attrs=False) def testGraphFromDictListExtraVertexName(self): - del self.vertices[2:] # No data for "Cecil" and "David" + del self.vertices[2:] # No data for "Cecil" and "David" g = Graph.DictList(self.vertices, self.edges) - self.assertTrue( - g.vcount() == 4 and g.ecount() == 5 and not g.is_directed() - ) + self.assertTrue(g.vcount() == 4 and g.ecount() == 5 and not g.is_directed()) self.assertTrue(g.vs["name"] == ["Alice", "Bob", "Cecil", "David"]) self.assertTrue(g.vs["age"] == [48, 33, None, None]) self.assertTrue(g.vs["gender"] == ["F", "M", None, None]) self.assertTrue(g.es["friendship"] == [4, 5, 5, 2, 1]) self.assertTrue(g.es["advice"] == [4, 5, 5, 4, 2]) - self.assertTrue(g.get_edgelist() == [ - (0, 1), (1, 2), (0, 2), (0, 3), (1, 3) - ]) + self.assertTrue(g.get_edgelist() == [(0, 1), (1, 2), (0, 2), (0, 3), (1, 3)]) def testGraphFromDictListAlternativeName(self): for vdata in self.vertices: @@ -510,18 +519,16 @@ def testGraphFromDictListAlternativeName(self): ) self.checkIfOK(g, "name_alternative") g = Graph.DictList( - self.vertices, self.edges, vertex_name_attr="name_alternative", - iterative=True + self.vertices, + self.edges, + vertex_name_attr="name_alternative", + iterative=True, ) self.checkIfOK(g, "name_alternative") def checkIfOK(self, g, name_attr, check_vertex_attrs=True): - self.assertTrue( - g.vcount() == 4 and g.ecount() == 5 and not g.is_directed() - ) - self.assertTrue(g.get_edgelist() == [ - (0, 1), (1, 2), (0, 2), (0, 3), (1, 3) - ]) + self.assertTrue(g.vcount() == 4 and g.ecount() == 5 and not g.is_directed()) + self.assertTrue(g.get_edgelist() == [(0, 1), (1, 2), (0, 2), (0, 3), (1, 3)]) self.assertTrue(g.vs[name_attr] == ["Alice", "Bob", "Cecil", "David"]) if check_vertex_attrs: self.assertTrue(g.vs["age"] == [48, 33, 45, 34]) @@ -537,7 +544,7 @@ def setUp(self): ("Cecil", "Bob", 5, 5), ("Cecil", "Alice", 5, 5), ("David", "Alice", 2, 4), - ("David", "Bob", 1, 2) + ("David", "Bob", 1, 2), ] def testGraphFromTupleList(self): @@ -547,10 +554,10 @@ def testGraphFromTupleList(self): def testGraphFromTupleListWithEdgeAttributes(self): g = Graph.TupleList(self.edges, edge_attrs=("friendship", "advice")) self.checkIfOK(g, "name", ("friendship", "advice")) - g = Graph.TupleList(self.edges, edge_attrs=("friendship", )) - self.checkIfOK(g, "name", ("friendship", )) + g = Graph.TupleList(self.edges, edge_attrs=("friendship",)) + self.checkIfOK(g, "name", ("friendship",)) g = Graph.TupleList(self.edges, edge_attrs="friendship") - self.checkIfOK(g, "name", ("friendship", )) + self.checkIfOK(g, "name", ("friendship",)) def testGraphFromTupleListWithDifferentNameAttribute(self): g = Graph.TupleList(self.edges, vertex_name_attr="spam") @@ -558,29 +565,26 @@ def testGraphFromTupleListWithDifferentNameAttribute(self): def testGraphFromTupleListWithWeights(self): g = Graph.TupleList(self.edges, weights=True) - self.checkIfOK(g, "name", ("weight", )) + self.checkIfOK(g, "name", ("weight",)) g = Graph.TupleList(self.edges, weights="friendship") - self.checkIfOK(g, "name", ("friendship", )) + self.checkIfOK(g, "name", ("friendship",)) g = Graph.TupleList(self.edges, weights=False) self.checkIfOK(g, "name", ()) self.assertRaises( - ValueError, Graph.TupleList, [self.edges], weights=True, - edge_attrs="friendship" + ValueError, + Graph.TupleList, + [self.edges], + weights=True, + edge_attrs="friendship", ) def testNoneForMissingAttributes(self): - g = Graph.TupleList( - self.edges, edge_attrs=("friendship", "advice", "spam") - ) + g = Graph.TupleList(self.edges, edge_attrs=("friendship", "advice", "spam")) self.checkIfOK(g, "name", ("friendship", "advice", "spam")) def checkIfOK(self, g, name_attr, edge_attrs): - self.assertTrue( - g.vcount() == 4 and g.ecount() == 5 and not g.is_directed() - ) - self.assertTrue(g.get_edgelist() == [ - (0, 1), (1, 2), (0, 2), (0, 3), (1, 3) - ]) + self.assertTrue(g.vcount() == 4 and g.ecount() == 5 and not g.is_directed()) + self.assertTrue(g.get_edgelist() == [(0, 1), (1, 2), (0, 2), (0, 3), (1, 3)]) self.assertTrue(g.attributes() == []) self.assertTrue(g.vertex_attributes() == [name_attr]) self.assertTrue(g.vs[name_attr] == ["Alice", "Bob", "Cecil", "David"]) @@ -609,14 +613,13 @@ def testIsDegreeSequence(self): self.assertFalse(is_degree_sequence([2, 1, -2])) self.assertFalse(is_degree_sequence([2, 1, 1, 1], [1, 1, 1, -2])) self.assertTrue(is_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3])) - self.assertTrue(is_degree_sequence( - [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], None - )) + self.assertTrue(is_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3], None)) self.assertFalse(is_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])) - self.assertTrue(is_degree_sequence( - [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], - [4, 3, 2, 3, 4, 4, 2, 2, 4, 2] - )) + self.assertTrue( + is_degree_sequence( + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [4, 3, 2, 3, 4, 4, 2, 2, 4, 2] + ) + ) def testIsGraphicalSequence(self): self.assertTrue(is_graphical_degree_sequence([])) @@ -627,26 +630,21 @@ def testIsGraphicalSequence(self): self.assertFalse(is_graphical_degree_sequence([1], [1])) self.assertFalse(is_graphical_degree_sequence([2])) self.assertFalse(is_graphical_degree_sequence([2, 1, 1, 1])) - self.assertTrue(is_graphical_degree_sequence( - [2, 1, 1, 1], [1, 1, 1, 2] - )) + self.assertTrue(is_graphical_degree_sequence([2, 1, 1, 1], [1, 1, 1, 2])) self.assertFalse(is_graphical_degree_sequence([2, 1, -2])) - self.assertFalse(is_graphical_degree_sequence( - [2, 1, 1, 1], [1, 1, 1, -2] - )) - self.assertTrue(is_graphical_degree_sequence( - [3, 3, 3, 3, 3, 3, 3, 3, 3, 3] - )) - self.assertTrue(is_graphical_degree_sequence( - [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], None - )) - self.assertFalse(is_graphical_degree_sequence( - [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3] - )) - self.assertTrue(is_graphical_degree_sequence( - [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], - [4, 3, 2, 3, 4, 4, 2, 2, 4, 2] - )) + self.assertFalse(is_graphical_degree_sequence([2, 1, 1, 1], [1, 1, 1, -2])) + self.assertTrue(is_graphical_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3])) + self.assertTrue( + is_graphical_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3], None) + ) + self.assertFalse( + is_graphical_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]) + ) + self.assertTrue( + is_graphical_degree_sequence( + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [4, 3, 2, 3, 4, 4, 2, 2, 4, 2] + ) + ) self.assertTrue(is_graphical_degree_sequence([3, 3, 3, 3, 4])) @@ -686,10 +684,16 @@ def suite(): graph_tuple_list_suite = unittest.makeSuite(GraphTupleListTests) degree_sequence_suite = unittest.makeSuite(DegreeSequenceTests) inheritance_suite = unittest.makeSuite(InheritanceTests) - return unittest.TestSuite([ - basic_suite, datatype_suite, graph_dict_list_suite, - graph_tuple_list_suite, degree_sequence_suite, inheritance_suite - ]) + return unittest.TestSuite( + [ + basic_suite, + datatype_suite, + graph_dict_list_suite, + graph_tuple_list_suite, + degree_sequence_suite, + inheritance_suite, + ] + ) def test(): diff --git a/tests/test_bipartite.py b/tests/test_bipartite.py index 74a57a002..283686cf6 100644 --- a/tests/test_bipartite.py +++ b/tests/test_bipartite.py @@ -1,99 +1,149 @@ import unittest from igraph import * + class BipartiteTests(unittest.TestCase): def testCreateBipartite(self): - g = Graph.Bipartite([0, 1]*5, [(0,1),(2,3),(4,5),(6,7),(8,9)]) - self.assertTrue(g.vcount() == 10 and g.ecount() == 5 and g.is_directed() == False) + g = Graph.Bipartite([0, 1] * 5, [(0, 1), (2, 3), (4, 5), (6, 7), (8, 9)]) + self.assertTrue( + g.vcount() == 10 and g.ecount() == 5 and g.is_directed() is False + ) self.assertTrue(g.is_bipartite()) - self.assertTrue(g.vs["type"] == [False, True]*5) + self.assertTrue(g.vs["type"] == [False, True] * 5) def testFullBipartite(self): g = Graph.Full_Bipartite(10, 5) - self.assertTrue(g.vcount() == 15 and g.ecount() == 50 and g.is_directed() == False) + self.assertTrue( + g.vcount() == 15 and g.ecount() == 50 and g.is_directed() is False + ) expected = sorted([(i, j) for i in range(10) for j in range(10, 15)]) self.assertTrue(sorted(g.get_edgelist()) == expected) - self.assertTrue(g.vs["type"] == [False]*10 + [True]*5) + self.assertTrue(g.vs["type"] == [False] * 10 + [True] * 5) g = Graph.Full_Bipartite(10, 5, directed=True, mode=OUT) - self.assertTrue(g.vcount() == 15 and g.ecount() == 50 and g.is_directed() == True) + self.assertTrue( + g.vcount() == 15 and g.ecount() == 50 and g.is_directed() is True + ) self.assertTrue(sorted(g.get_edgelist()) == expected) - self.assertTrue(g.vs["type"] == [False]*10 + [True]*5) + self.assertTrue(g.vs["type"] == [False] * 10 + [True] * 5) g = Graph.Full_Bipartite(10, 5, directed=True, mode=IN) - self.assertTrue(g.vcount() == 15 and g.ecount() == 50 and g.is_directed() == True) - self.assertTrue(sorted(g.get_edgelist()) == sorted([(i,j) for j, i in expected])) - self.assertTrue(g.vs["type"] == [False]*10 + [True]*5) + self.assertTrue( + g.vcount() == 15 and g.ecount() == 50 and g.is_directed() is True + ) + self.assertTrue( + sorted(g.get_edgelist()) == sorted([(i, j) for j, i in expected]) + ) + self.assertTrue(g.vs["type"] == [False] * 10 + [True] * 5) g = Graph.Full_Bipartite(10, 5, directed=True) - self.assertTrue(g.vcount() == 15 and g.ecount() == 100 and g.is_directed() == True) + self.assertTrue( + g.vcount() == 15 and g.ecount() == 100 and g.is_directed() is True + ) expected.extend([(j, i) for i, j in expected]) expected.sort() self.assertTrue(sorted(g.get_edgelist()) == expected) - self.assertTrue(g.vs["type"] == [False]*10 + [True]*5) + self.assertTrue(g.vs["type"] == [False] * 10 + [True] * 5) def testIncidence(self): g = Graph.Incidence([[0, 1, 1], [1, 2, 0]]) self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) - self.assertListEqual(sorted(g.get_edgelist()), [(0,3),(0,4),(1,2),(1,3)]) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], multiple=True) self.assertTrue(all((g.vcount() == 5, g.ecount() == 5, not g.is_directed()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) - self.assertListEqual(sorted(g.get_edgelist()), [(0,3),(0,4),(1,2),(1,3),(1,3)]) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) + self.assertListEqual( + sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3), (1, 3)] + ) g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True) self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) - self.assertListEqual(sorted(g.get_edgelist()), [(0,3),(0,4),(1,2),(1,3)]) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, mode="in") self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) - self.assertListEqual(sorted(g.get_edgelist()), [(2,1),(3,0),(3,1),(4,0)]) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) + self.assertListEqual(sorted(g.get_edgelist()), [(2, 1), (3, 0), (3, 1), (4, 0)]) # Create a weighted Graph g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], weighted=True) - self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed(), g.is_weighted()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertTrue( + all( + (g.vcount() == 5, g.ecount() == 4, not g.is_directed(), g.is_weighted()) + ) + ) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) # Graph is not weighted when weighted=`str` g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], weighted="some_attr_name") - self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed(), not g.is_weighted()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertTrue( + all( + ( + g.vcount() == 5, + g.ecount() == 4, + not g.is_directed(), + not g.is_weighted(), + ) + ) + ) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) self.assertListEqual(g.es["some_attr_name"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) # Graph is not weighted when weighted="" g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], weighted="") - self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, not g.is_directed(), not g.is_weighted()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertTrue( + all( + ( + g.vcount() == 5, + g.ecount() == 4, + not g.is_directed(), + not g.is_weighted(), + ) + ) + ) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) self.assertListEqual(g.es[""], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) # Should work when directed=True and mode=out with weighted g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, weighted=True) - self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + self.assertTrue( + all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted())) + ) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2),(1,3)]) + self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3)]) # Should work when directed=True and mode=in with weighted - g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, mode="in", weighted=True) - self.assertTrue(all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + g = Graph.Incidence( + [[0, 1, 1], [1, 2, 0]], directed=True, mode="in", weighted=True + ) + self.assertTrue( + all((g.vcount() == 5, g.ecount() == 4, g.is_directed(), g.is_weighted())) + ) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) self.assertListEqual(g.es["weight"], [1, 1, 1, 2]) self.assertListEqual(sorted(g.get_edgelist()), [(2, 1), (3, 0), (3, 1), (4, 0)]) # Should work when directed=True and mode=all with weighted - g = Graph.Incidence([[0, 1, 1], [1, 2, 0]], directed=True, mode="all", weighted=True) - self.assertTrue(all((g.vcount() == 5, g.ecount() == 8, g.is_directed(), g.is_weighted()))) - self.assertListEqual(g.vs["type"], [False]*2 + [True]*3) + g = Graph.Incidence( + [[0, 1, 1], [1, 2, 0]], directed=True, mode="all", weighted=True + ) + self.assertTrue( + all((g.vcount() == 5, g.ecount() == 8, g.is_directed(), g.is_weighted())) + ) + self.assertListEqual(g.vs["type"], [False] * 2 + [True] * 3) self.assertListEqual(g.es["weight"], [1, 1, 1, 1, 1, 1, 2, 2]) - self.assertListEqual(sorted(g.get_edgelist()), [(0, 3), (0, 4), (1, 2), (1, 3), (2, 1), (3, 0), (3, 1), (4, 0)]) + self.assertListEqual( + sorted(g.get_edgelist()), + [(0, 3), (0, 4), (1, 2), (1, 3), (2, 1), (3, 0), (3, 1), (4, 0)], + ) def testIncidenceError(self): msg = "arguments weighted and multiple can not co-exist" @@ -152,26 +202,28 @@ def testBipartiteProjection(self): def testIsBipartite(self): g = Graph.Star(10) - self.assertTrue(g.is_bipartite() == True) - self.assertTrue(g.is_bipartite(True) == (True, [False] + [True]*9)) + self.assertTrue(g.is_bipartite() is True) + self.assertTrue(g.is_bipartite(True) == (True, [False] + [True] * 9)) g = Graph.Tree(100, 3) - self.assertTrue(g.is_bipartite() == True) + self.assertTrue(g.is_bipartite() is True) g = Graph.Ring(9) - self.assertTrue(g.is_bipartite() == False) + self.assertTrue(g.is_bipartite() is False) self.assertTrue(g.is_bipartite(True) == (False, None)) g = Graph.Ring(10) - self.assertTrue(g.is_bipartite() == True) + self.assertTrue(g.is_bipartite() is True) g += (2, 0) self.assertTrue(g.is_bipartite(True) == (False, None)) - + + def suite(): bipartite_suite = unittest.makeSuite(BipartiteTests) return unittest.TestSuite([bipartite_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 1cb2de48e..f972fd715 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import unittest from igraph import * @@ -9,48 +7,89 @@ class CliqueTests(unittest.TestCase): def setUp(self): - self.g=Graph.Full(6) + self.g = Graph.Full(6) self.g.delete_edges([(0, 1), (0, 2), (3, 5)]) def testCliques(self): - tests = {(4, -1): [[1, 2, 3, 4], [1, 2, 4, 5]], - (2, 2): [[0, 3], [0, 4], [0, 5], - [1, 2], [1, 3], [1, 4], [1, 5], - [2, 3], [2, 4], [2, 5], [3, 4], [4, 5]], - (-1, -1): [[0], [1], [2], [3], [4], [5], - [0, 3], [0, 4], [0, 5], - [1, 2], [1, 3], [1, 4], [1, 5], - [2, 3], [2, 4], [2, 5], [3, 4], [4, 5], - [0, 3, 4], [0, 4, 5], - [1, 2, 3], [1, 2, 4], [1, 2, 5], - [1, 3, 4], [1, 4, 5], [2, 3, 4], [2, 4, 5], - [1, 2, 3, 4], [1, 2, 4, 5]]} - for (lo, hi), exp in tests.items(): + tests = { + (4, -1): [[1, 2, 3, 4], [1, 2, 4, 5]], + (2, 2): [ + [0, 3], + [0, 4], + [0, 5], + [1, 2], + [1, 3], + [1, 4], + [1, 5], + [2, 3], + [2, 4], + [2, 5], + [3, 4], + [4, 5], + ], + (-1, -1): [ + [0], + [1], + [2], + [3], + [4], + [5], + [0, 3], + [0, 4], + [0, 5], + [1, 2], + [1, 3], + [1, 4], + [1, 5], + [2, 3], + [2, 4], + [2, 5], + [3, 4], + [4, 5], + [0, 3, 4], + [0, 4, 5], + [1, 2, 3], + [1, 2, 4], + [1, 2, 5], + [1, 3, 4], + [1, 4, 5], + [2, 3, 4], + [2, 4, 5], + [1, 2, 3, 4], + [1, 2, 4, 5], + ], + } + for (lo, hi), exp in list(tests.items()): self.assertEqual(sorted(exp), sorted(map(sorted, self.g.cliques(lo, hi)))) def testLargestCliques(self): - self.assertEqual(sorted(map(sorted, self.g.largest_cliques())), - [[1, 2, 3, 4], [1, 2, 4, 5]]) + self.assertEqual( + sorted(map(sorted, self.g.largest_cliques())), [[1, 2, 3, 4], [1, 2, 4, 5]] + ) def testMaximalCliques(self): - self.assertEqual(sorted(map(sorted, self.g.maximal_cliques())), - [[0, 3, 4], [0, 4, 5], - [1, 2, 3, 4], [1, 2, 4, 5]]) - self.assertEqual(sorted(map(sorted, self.g.maximal_cliques(min=4))), - [[1, 2, 3, 4], [1, 2, 4, 5]]) - self.assertEqual(sorted(map(sorted, self.g.maximal_cliques(max=3))), - [[0, 3, 4], [0, 4, 5]]) + self.assertEqual( + sorted(map(sorted, self.g.maximal_cliques())), + [[0, 3, 4], [0, 4, 5], [1, 2, 3, 4], [1, 2, 4, 5]], + ) + self.assertEqual( + sorted(map(sorted, self.g.maximal_cliques(min=4))), + [[1, 2, 3, 4], [1, 2, 4, 5]], + ) + self.assertEqual( + sorted(map(sorted, self.g.maximal_cliques(max=3))), [[0, 3, 4], [0, 4, 5]] + ) def testMaximalCliquesFile(self): def read_cliques(fname): with open(fname) as fp: - return sorted(sorted(int(item) for item in line.split()) - for line in fp) + return sorted(sorted(int(item) for item in line.split()) for line in fp) with temporary_file() as fname: self.g.maximal_cliques(file=fname) - self.assertEqual([[0, 3, 4], [0, 4, 5], [1, 2, 3, 4], [1, 2, 4, 5]], - read_cliques(fname)) + self.assertEqual( + [[0, 3, 4], [0, 4, 5], [1, 2, 3, 4], [1, 2, 4, 5]], read_cliques(fname) + ) with temporary_file() as fname: self.g.maximal_cliques(min=4, file=fname) @@ -67,28 +106,52 @@ def testCliqueNumber(self): class IndependentVertexSetTests(unittest.TestCase): def setUp(self): - self.g1=Graph.Tree(5, 2, TREE_UNDIRECTED) - self.g2=Graph.Tree(10, 2, TREE_UNDIRECTED) + self.g1 = Graph.Tree(5, 2, TREE_UNDIRECTED) + self.g2 = Graph.Tree(10, 2, TREE_UNDIRECTED) def testIndependentVertexSets(self): - tests = {(4, -1): [], - (2, 2): [(0, 3), (0, 4), (1, 2), (2, 3), (2, 4), (3, 4)], - (-1, -1): [(0,), (1,), (2,), (3,), (4,), - (0, 3), (0, 4), (1, 2), (2, 3), (2, 4), - (3, 4), (0, 3, 4), (2, 3, 4)]} - for (lo, hi), exp in tests.items(): + tests = { + (4, -1): [], + (2, 2): [(0, 3), (0, 4), (1, 2), (2, 3), (2, 4), (3, 4)], + (-1, -1): [ + (0,), + (1,), + (2,), + (3,), + (4,), + (0, 3), + (0, 4), + (1, 2), + (2, 3), + (2, 4), + (3, 4), + (0, 3, 4), + (2, 3, 4), + ], + } + for (lo, hi), exp in list(tests.items()): self.assertEqual(exp, self.g1.independent_vertex_sets(lo, hi)) def testLargestIndependentVertexSets(self): - self.assertEqual(self.g1.largest_independent_vertex_sets(), - [(0, 3, 4), (2, 3, 4)]) + self.assertEqual( + self.g1.largest_independent_vertex_sets(), [(0, 3, 4), (2, 3, 4)] + ) def testMaximalIndependentVertexSets(self): - self.assertEqual(self.g2.maximal_independent_vertex_sets(), - [(0, 3, 4, 5, 6), (0, 3, 5, 6, 9), - (0, 4, 5, 6, 7, 8), (0, 5, 6, 7, 8, 9), - (1, 2, 7, 8, 9), (1, 5, 6, 7, 8, 9), - (2, 3, 4), (2, 3, 9), (2, 4, 7, 8)]) + self.assertEqual( + self.g2.maximal_independent_vertex_sets(), + [ + (0, 3, 4, 5, 6), + (0, 3, 5, 6, 9), + (0, 4, 5, 6, 7, 8), + (0, 5, 6, 7, 8, 9), + (1, 2, 7, 8, 9), + (1, 5, 6, 7, 8, 9), + (2, 3, 4), + (2, 3, 9), + (2, 4, 7, 8), + ], + ) def testIndependenceNumber(self): self.assertEqual(self.g2.independence_number(), 6) @@ -122,13 +185,14 @@ def testTriads(self): tc = self.g.triad_census() accessors = ["003", "012", "021d", "030C"] for a in accessors: - self.assertTrue(isinstance(getattr(tc, "t"+a), int)) + self.assertTrue(isinstance(getattr(tc, "t" + a), int)) self.assertTrue(isinstance(tc[a], int)) self.assertTrue(isinstance(list(tc), list)) self.assertTrue(isinstance(tuple(tc), tuple)) self.assertTrue(len(list(tc)) == 16) self.assertTrue(len(tuple(tc)) == 16) + class CliqueBenchmark(object): """This is a benchmark, not a real test case. You can run it using: @@ -140,6 +204,7 @@ class CliqueBenchmark(object): def __init__(self): from time import time import gc + self.time = time self.gc_collect = gc.collect @@ -163,15 +228,17 @@ def timeit(self, g): cl = g.maximal_cliques() end = self.time() self.gc_collect() - return len(cl), mid-start, end-mid + return len(cl), mid - start, end - mid def testRandom(self): - np = {100: [0.6, 0.7], - 300: [0.1, 0.2, 0.3, 0.4], - 500: [0.1, 0.2, 0.3], - 700: [0.1, 0.2], - 1000:[0.1, 0.2], - 10000: [0.001, 0.003, 0.005, 0.01, 0.02]} + np = { + 100: [0.6, 0.7], + 300: [0.1, 0.2, 0.3, 0.4], + 500: [0.1, 0.2, 0.3], + 700: [0.1, 0.2], + 1000: [0.1, 0.2], + 10000: [0.001, 0.003, 0.005, 0.01, 0.02], + } print() print("Erdos-Renyi random graphs") @@ -189,12 +256,19 @@ def testMoonMoser(self): print("Moon-Moser graphs") print(" n exp_clqs #cliques t1 t2") for n in ns: - n3 = n/3 - types = range(n3) * 3 - el = [(i, j) for i in range(n) for j in range(i+1,n) if types[i] != types[j]] + n3 = n / 3 + types = list(range(n3)) * 3 + el = [ + (i, j) + for i in range(n) + for j in range(i + 1, n) + if types[i] != types[j] + ] g = Graph(n, el) result = self.timeit(g) - print("%8d %8d %8d %8.4fs %8.4fs" % tuple([n, (3**(n/3))] + list(result))) + print( + "%8d %8d %8d %8.4fs %8.4fs" % tuple([n, (3 ** (n / 3))] + list(result)) + ) def testGRG(self): ns = [100, 1000, 5000, 10000, 25000, 50000] @@ -203,7 +277,7 @@ def testGRG(self): print("Geometric random graphs") print(" n d #cliques t1 t2") for n in ns: - d = 2. / (n ** 0.5) + d = 2.0 / (n ** 0.5) g = Graph.GRG(n, d) result = self.timeit(g) print("%8d %8.3f %8d %8.4fs %8.4fs" % tuple([n, d] + list(result))) @@ -215,10 +289,11 @@ def suite(): motif_suite = unittest.makeSuite(MotifTests) return unittest.TestSuite([clique_suite, indvset_suite, motif_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_colortests.py b/tests/test_colortests.py index df7332ef1..4cf99954c 100644 --- a/tests/test_colortests.py +++ b/tests/test_colortests.py @@ -1,42 +1,42 @@ import unittest -try: - from itertools import izip -except ImportError: - izip = zip # Python 3.x - from igraph import * + class ColorTests(unittest.TestCase): def assertAlmostEqualMany(self, items1, items2, eps): - for idx, (item1, item2) in enumerate(izip(items1, items2)): - self.assertAlmostEqual(item1, item2, places=eps, - msg="mismatch at index %d, %r != %r with %d digits" - % (idx, items1, items2, eps)) + for idx, (item1, item2) in enumerate(zip(items1, items2)): + self.assertAlmostEqual( + item1, + item2, + places=eps, + msg="mismatch at index %d, %r != %r with %d digits" + % (idx, items1, items2, eps), + ) def setUp(self): columns = ["r", "g", "b", "h", "v", "l", "s_hsv", "s_hsl", "alpha"] # Examples taken from http://en.wikipedia.org/wiki/HSL_and_HSV values = [ - (1, 1, 1, 0, 1, 1, 0, 0, 1), - (0.5, 0.5, 0.5, 0, 0.5, 0.5, 0, 0, 0.5), - (0, 0, 0, 0, 0, 0, 0, 0, 1), - (1, 0, 0, 0, 1, 0.5, 1, 1, 0.5), - (0.75, 0.75, 0, 60, 0.75, 0.375, 1, 1, 0.25), - (0, 0.5, 0, 120, 0.5, 0.25, 1, 1, 0.75), - (0.5, 1, 1, 180, 1, 0.75, 0.5, 1, 1), - (0.5, 0.5, 1, 240, 1, 0.75, 0.5, 1, 1), - (0.75, 0.25, 0.75, 300, 0.75, 0.5, 0.666666667, 0.5, 0.25), - (0.211, 0.149, 0.597, 248.3, 0.597, 0.373, 0.750, 0.601, 1), - (0.495, 0.493, 0.721, 240.5, 0.721, 0.607, 0.316, 0.290, 0.75), + (1, 1, 1, 0, 1, 1, 0, 0, 1), + (0.5, 0.5, 0.5, 0, 0.5, 0.5, 0, 0, 0.5), + (0, 0, 0, 0, 0, 0, 0, 0, 1), + (1, 0, 0, 0, 1, 0.5, 1, 1, 0.5), + (0.75, 0.75, 0, 60, 0.75, 0.375, 1, 1, 0.25), + (0, 0.5, 0, 120, 0.5, 0.25, 1, 1, 0.75), + (0.5, 1, 1, 180, 1, 0.75, 0.5, 1, 1), + (0.5, 0.5, 1, 240, 1, 0.75, 0.5, 1, 1), + (0.75, 0.25, 0.75, 300, 0.75, 0.5, 0.666666667, 0.5, 0.25), + (0.211, 0.149, 0.597, 248.3, 0.597, 0.373, 0.750, 0.601, 1), + (0.495, 0.493, 0.721, 240.5, 0.721, 0.607, 0.316, 0.290, 0.75), ] - self.data = [dict(zip(columns, value)) for value in values] + self.data = [dict(list(zip(columns, value))) for value in values] for row in self.data: - row["h"] /= 360. + row["h"] /= 360.0 def _testGeneric(self, method, args1, args2=("r", "g", "b")): - if len(args1) == len(args2)+1: - args2 += ("alpha", ) + if len(args1) == len(args2) + 1: + args2 += ("alpha",) for data in self.data: vals1 = [data.get(arg, 0.0) for arg in args1] vals2 = [data.get(arg, 0.0) for arg in args2] @@ -58,34 +58,38 @@ def testRGBtoHSL(self): self._testGeneric(rgb_to_hsl, "r g b".split(), "h s_hsl l".split()) def testRGBAtoHSLA(self): - self._testGeneric(rgba_to_hsla, "r g b alpha".split(), "h s_hsl l alpha".split()) + self._testGeneric( + rgba_to_hsla, "r g b alpha".split(), "h s_hsl l alpha".split() + ) def testRGBtoHSV(self): self._testGeneric(rgb_to_hsv, "r g b".split(), "h s_hsv v".split()) def testRGBAtoHSVA(self): - self._testGeneric(rgba_to_hsva, "r g b alpha".split(), "h s_hsv v alpha".split()) + self._testGeneric( + rgba_to_hsva, "r g b alpha".split(), "h s_hsv v alpha".split() + ) class PaletteTests(unittest.TestCase): def testGradientPalette(self): gp = GradientPalette("red", "blue", 3) - self.assertTrue(gp.get(0) == (1., 0., 0., 1.)) - self.assertTrue(gp.get(1) == (0.5, 0., 0.5, 1.)) - self.assertTrue(gp.get(2) == (0., 0., 1., 1.)) + self.assertTrue(gp.get(0) == (1.0, 0.0, 0.0, 1.0)) + self.assertTrue(gp.get(1) == (0.5, 0.0, 0.5, 1.0)) + self.assertTrue(gp.get(2) == (0.0, 0.0, 1.0, 1.0)) def testAdvancedGradientPalette(self): agp = AdvancedGradientPalette(["red", "black", "blue"], n=9) - self.assertTrue(agp.get(0) == (1., 0., 0., 1.)) - self.assertTrue(agp.get(2) == (0.5, 0., 0., 1.)) - self.assertTrue(agp.get(4) == (0., 0., 0., 1.)) - self.assertTrue(agp.get(5) == (0., 0., 0.25, 1.)) - self.assertTrue(agp.get(8) == (0., 0., 1., 1.)) + self.assertTrue(agp.get(0) == (1.0, 0.0, 0.0, 1.0)) + self.assertTrue(agp.get(2) == (0.5, 0.0, 0.0, 1.0)) + self.assertTrue(agp.get(4) == (0.0, 0.0, 0.0, 1.0)) + self.assertTrue(agp.get(5) == (0.0, 0.0, 0.25, 1.0)) + self.assertTrue(agp.get(8) == (0.0, 0.0, 1.0, 1.0)) agp = AdvancedGradientPalette(["red", "black", "blue"], [0, 8, 2], 9) - self.assertTrue(agp.get(0) == (1., 0., 0., 1.)) - self.assertTrue(agp.get(1) == (0.5, 0., 0.5, 1.)) - self.assertTrue(agp.get(5) == (0., 0., 0.5, 1.)) + self.assertTrue(agp.get(0) == (1.0, 0.0, 0.0, 1.0)) + self.assertTrue(agp.get(1) == (0.5, 0.0, 0.5, 1.0)) + self.assertTrue(agp.get(5) == (0.0, 0.0, 0.5, 1.0)) def suite(): @@ -93,10 +97,11 @@ def suite(): palette_suite = unittest.makeSuite(PaletteTests) return unittest.TestSuite([color_suite, palette_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 8a00cbe09..6c3860806 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -1,46 +1,50 @@ import unittest from igraph import * + class DirectedUndirectedTests(unittest.TestCase): def testToUndirected(self): - graph = Graph([(0,1), (0,2), (1,0)], directed=True) + graph = Graph([(0, 1), (0, 2), (1, 0)], directed=True) graph2 = graph.copy() graph2.to_undirected(mode=False) self.assertTrue(graph2.vcount() == graph.vcount()) self.assertTrue(graph2.is_directed() == False) - self.assertTrue(sorted(graph2.get_edgelist()) == [(0,1), (0,1), (0,2)]) + self.assertTrue(sorted(graph2.get_edgelist()) == [(0, 1), (0, 1), (0, 2)]) graph2 = graph.copy() graph2.to_undirected() self.assertTrue(graph2.vcount() == graph.vcount()) self.assertTrue(graph2.is_directed() == False) - self.assertTrue(sorted(graph2.get_edgelist()) == [(0,1), (0,2)]) + self.assertTrue(sorted(graph2.get_edgelist()) == [(0, 1), (0, 2)]) graph2 = graph.copy() - graph2.es["weight"] = [1,2,3] + graph2.es["weight"] = [1, 2, 3] graph2.to_undirected(mode="collapse", combine_edges="sum") self.assertTrue(graph2.vcount() == graph.vcount()) self.assertTrue(graph2.is_directed() == False) - self.assertTrue(sorted(graph2.get_edgelist()) == [(0,1), (0,2)]) - self.assertTrue(graph2.es["weight"] == [4,2]) + self.assertTrue(sorted(graph2.get_edgelist()) == [(0, 1), (0, 2)]) + self.assertTrue(graph2.es["weight"] == [4, 2]) - graph = Graph([(0,1),(1,0),(0,1),(1,0),(2,1),(1,2)], directed=True) + graph = Graph([(0, 1), (1, 0), (0, 1), (1, 0), (2, 1), (1, 2)], directed=True) graph2 = graph.copy() - graph2.es["weight"] = [1,2,3,4,5,6] + graph2.es["weight"] = [1, 2, 3, 4, 5, 6] graph2.to_undirected(mode="mutual", combine_edges="sum") self.assertTrue(graph2.vcount() == graph.vcount()) self.assertTrue(graph2.is_directed() == False) - self.assertTrue(sorted(graph2.get_edgelist()) == [(0,1), (0,1), (1,2)]) - self.assertTrue(graph2.es["weight"] == [7,3,11] or graph2.es["weight"] == [3,7,11]) + self.assertTrue(sorted(graph2.get_edgelist()) == [(0, 1), (0, 1), (1, 2)]) + self.assertTrue( + graph2.es["weight"] == [7, 3, 11] or graph2.es["weight"] == [3, 7, 11] + ) def testToDirected(self): - graph = Graph([(0,1), (0,2), (2,3), (2,4)], directed=False) + graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False) graph.to_directed() self.assertTrue(graph.is_directed()) self.assertTrue(graph.vcount() == 5) - self.assertTrue(sorted(graph.get_edgelist()) == \ - [(0,1), (0,2), (1,0), (2,0), (2,3), (2,4), (3,2), (4,2)] + self.assertTrue( + sorted(graph.get_edgelist()) + == [(0, 1), (0, 2), (1, 0), (2, 0), (2, 3), (2, 4), (3, 2), (4, 2)] ) @@ -48,43 +52,64 @@ class GraphRepresentationTests(unittest.TestCase): def testGetAdjacency(self): # Undirected case g = Graph.Tree(6, 3) - g.es["weight"] = range(5) - self.assertTrue(g.get_adjacency() == Matrix([ - [0, 1, 1, 1, 0, 0], - [1, 0, 0, 0, 1, 1], - [1, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0] - ])) - self.assertTrue(g.get_adjacency(attribute="weight") == Matrix([ - [0, 0, 1, 2, 0, 0], - [0, 0, 0, 0, 3, 4], - [1, 0, 0, 0, 0, 0], - [2, 0, 0, 0, 0, 0], - [0, 3, 0, 0, 0, 0], - [0, 4, 0, 0, 0, 0] - ])) - self.assertTrue(g.get_adjacency(eids=True) == Matrix([ - [0, 1, 2, 3, 0, 0], - [1, 0, 0, 0, 4, 5], - [2, 0, 0, 0, 0, 0], - [3, 0, 0, 0, 0, 0], - [0, 4, 0, 0, 0, 0], - [0, 5, 0, 0, 0, 0] - ])-1) + g.es["weight"] = list(range(5)) + self.assertTrue( + g.get_adjacency() + == Matrix( + [ + [0, 1, 1, 1, 0, 0], + [1, 0, 0, 0, 1, 1], + [1, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + [0, 1, 0, 0, 0, 0], + ] + ) + ) + self.assertTrue( + g.get_adjacency(attribute="weight") + == Matrix( + [ + [0, 0, 1, 2, 0, 0], + [0, 0, 0, 0, 3, 4], + [1, 0, 0, 0, 0, 0], + [2, 0, 0, 0, 0, 0], + [0, 3, 0, 0, 0, 0], + [0, 4, 0, 0, 0, 0], + ] + ) + ) + self.assertTrue( + g.get_adjacency(eids=True) + == Matrix( + [ + [0, 1, 2, 3, 0, 0], + [1, 0, 0, 0, 4, 5], + [2, 0, 0, 0, 0, 0], + [3, 0, 0, 0, 0, 0], + [0, 4, 0, 0, 0, 0], + [0, 5, 0, 0, 0, 0], + ] + ) + - 1 + ) # Directed case g = Graph.Tree(6, 3, "tree_out") - g.add_edges([(0,1), (1,0)]) - self.assertTrue(g.get_adjacency() == Matrix([ - [0, 2, 1, 1, 0, 0], - [1, 0, 0, 0, 1, 1], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0] - ])) + g.add_edges([(0, 1), (1, 0)]) + self.assertTrue( + g.get_adjacency() + == Matrix( + [ + [0, 2, 1, 1, 0, 0], + [1, 0, 0, 0, 1, 1], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0], + ] + ) + ) def testGetSparseAdjacency(self): try: @@ -95,32 +120,37 @@ def testGetSparseAdjacency(self): # Undirected case g = Graph.Tree(6, 3) - g.es["weight"] = range(5) - self.assertTrue(np.all( - (g.get_adjacency_sparse() == np.array(g.get_adjacency().data)) - )) - self.assertTrue(np.all( - (g.get_adjacency_sparse(attribute="weight") == np.array(g.get_adjacency(attribute="weight").data)) - )) + g.es["weight"] = list(range(5)) + self.assertTrue( + np.all((g.get_adjacency_sparse() == np.array(g.get_adjacency().data))) + ) + self.assertTrue( + np.all( + ( + g.get_adjacency_sparse(attribute="weight") + == np.array(g.get_adjacency(attribute="weight").data) + ) + ) + ) # Directed case g = Graph.Tree(6, 3, "tree_out") - g.add_edges([(0,1), (1,0)]) - self.assertTrue(np.all( - g.get_adjacency_sparse() == np.array(g.get_adjacency().data) - )) + g.add_edges([(0, 1), (1, 0)]) + self.assertTrue( + np.all(g.get_adjacency_sparse() == np.array(g.get_adjacency().data)) + ) def suite(): direction_suite = unittest.makeSuite(DirectedUndirectedTests) representation_suite = unittest.makeSuite(GraphRepresentationTests) - return unittest.TestSuite([direction_suite, - representation_suite]) + return unittest.TestSuite([direction_suite, representation_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 1f48c2f09..3eaef38fb 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -3,27 +3,31 @@ import math from igraph import * + try: set, frozenset except NameError: import sets + set, frozenset = sets.Set, sets.ImmutableSet class SubgraphTests(unittest.TestCase): def testSubgraph(self): g = Graph.Lattice([10, 10], circular=False, mutual=False) - g.vs["id"] = range(g.vcount()) + g.vs["id"] = list(range(g.vcount())) vs = [0, 1, 2, 10, 11, 12, 20, 21, 22] sg = g.subgraph(vs) - self.assertTrue(sg.isomorphic(Graph.Lattice([3, 3], circular=False, mutual=False))) + self.assertTrue( + sg.isomorphic(Graph.Lattice([3, 3], circular=False, mutual=False)) + ) self.assertTrue(sg.vs["id"] == vs) def testSubgraphEdges(self): g = Graph.Lattice([10, 10], circular=False, mutual=False) - g.es["id"] = range(g.ecount()) + g.es["id"] = list(range(g.ecount())) es = [0, 1, 2, 5, 20, 21, 22, 24, 38, 40] sg = g.subgraph_edges(es) @@ -36,20 +40,37 @@ def testSubgraphEdges(self): class DecompositionTests(unittest.TestCase): def testKCores(self): - g = Graph(11, [(0,1), (0,2), (0,3), (1,2), (1,3), (2,3), - (2,4), (2,5), (3,6), (3,7), (1,7), (7,8), - (1,9), (1,10), (9,10)]) - self.assertTrue(g.coreness() == [3,3,3,3,1,1,1,2,1,2,2]) + g = Graph( + 11, + [ + (0, 1), + (0, 2), + (0, 3), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (2, 5), + (3, 6), + (3, 7), + (1, 7), + (7, 8), + (1, 9), + (1, 10), + (9, 10), + ], + ) + self.assertTrue(g.coreness() == [3, 3, 3, 3, 1, 1, 1, 2, 1, 2, 2]) self.assertTrue(g.shell_index() == g.coreness()) - l=g.k_core(3).get_edgelist() + l = g.k_core(3).get_edgelist() l.sort() - self.assertTrue(l == [(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)]) + self.assertTrue(l == [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]) class ClusteringTests(unittest.TestCase): def setUp(self): - self.cl = Clustering([0,0,0,1,1,2,1,1,4,4]) + self.cl = Clustering([0, 0, 0, 1, 1, 2, 1, 1, 4, 4]) def testClusteringIndex(self): self.assertTrue(self.cl[0] == [0, 1, 2]) @@ -62,7 +83,7 @@ def testClusteringLength(self): self.assertTrue(len(self.cl) == 5) def testClusteringMembership(self): - self.assertTrue(self.cl.membership == [0,0,0,1,1,2,1,1,4,4]) + self.assertTrue(self.cl.membership == [0, 0, 0, 1, 1, 2, 1, 1, 4, 4]) def testClusteringSizes(self): self.assertTrue(self.cl.sizes() == [3, 4, 1, 0, 2]) @@ -81,7 +102,7 @@ def setUp(self): def testFromStringAttribute(self): cl = VertexClustering.FromAttribute(self.graph, "string") - self.assertTrue(cl.membership == [0,0,0,1,1,2,2,2,0,1]) + self.assertTrue(cl.membership == [0, 0, 0, 1, 1, 2, 2, 2, 0, 1]) def testFromIntAttribute(self): cl = VertexClustering.FromAttribute(self.graph, "int") @@ -93,7 +114,7 @@ def testFromIntAttribute(self): def testClusterGraph(self): cl = VertexClustering(self.graph, [0, 0, 0, 1, 1, 1, 2, 2, 2, 2]) - self.graph.delete_edges(self.graph.es.select(_between=([0,1,2], [3,4,5]))) + self.graph.delete_edges(self.graph.es.select(_between=([0, 1, 2], [3, 4, 5]))) clg = cl.cluster_graph(dict(string="concat", int=max)) self.assertTrue(sorted(clg.get_edgelist()) == [(0, 2), (1, 2)]) @@ -102,8 +123,14 @@ def testClusterGraph(self): self.assertTrue(clg.vs["int"] == [41, 64, 47]) clg = cl.cluster_graph(dict(string="concat", int=max), False) - self.assertTrue(sorted(clg.get_edgelist()) == \ - [(0, 0)]*3 + [(0, 2)]*12 + [(1, 1)]*3 + [(1, 2)]*12 + [(2, 2)]*6) + self.assertTrue( + sorted(clg.get_edgelist()) + == [(0, 0)] * 3 + + [(0, 2)] * 12 + + [(1, 1)] * 3 + + [(1, 2)] * 12 + + [(2, 2)] * 6 + ) self.assertTrue(not clg.is_directed()) self.assertTrue(clg.vs["string"] == ["aaa", "bbc", "ccab"]) self.assertTrue(clg.vs["int"] == [41, 64, 47]) @@ -111,7 +138,7 @@ def testClusterGraph(self): class CoverTests(unittest.TestCase): def setUp(self): - self.cl = Cover([(0,1,2,3), (3,4,5,6,9), (), (8,9)]) + self.cl = Cover([(0, 1, 2, 3), (3, 4, 5, 6, 9), (), (8, 9)]) def testCoverIndex(self): self.assertTrue(self.cl[0] == [0, 1, 2, 3]) @@ -133,9 +160,9 @@ def testCoverHistogram(self): def testCoverConstructorWithN(self): self.assertTrue(self.cl.n == 10) - cl = Cover(self.cl, n = 15) + cl = Cover(self.cl, n=15) self.assertTrue(cl.n == 15) - cl = Cover(self.cl, n = 1) + cl = Cover(self.cl, n=1) self.assertTrue(cl.n == 10) @@ -151,29 +178,30 @@ def assertMembershipsEqual(self, observed, expected): observed = observed.membership if hasattr(expected, "membership"): expected = expected.membership - self.assertEqual(self.reindexMembership(expected), \ - self.reindexMembership(observed)) + self.assertEqual( + self.reindexMembership(expected), self.reindexMembership(observed) + ) def testClauset(self): # Two cliques of size 5 with one connecting edge g = Graph.Full(5) + Graph.Full(5) g.add_edges([(0, 5)]) cl = g.community_fastgreedy().as_clustering() - self.assertMembershipsEqual(cl, [0,0,0,0,0,1,1,1,1,1]) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cl.q, 0.4523, places=3) # Lollipop, weighted g = Graph.Full(4) + Graph.Full(2) - g.add_edges([(3,4)]) + g.add_edges([(3, 4)]) weights = [1, 1, 1, 1, 1, 1, 10, 10] cl = g.community_fastgreedy(weights).as_clustering() - self.assertMembershipsEqual(cl, [0,0,0,1,1,1]) + self.assertMembershipsEqual(cl, [0, 0, 0, 1, 1, 1]) self.assertAlmostEqual(cl.q, 0.1708, places=3) # Same graph, different weights g.es["weight"] = [3] * g.ecount() cl = g.community_fastgreedy("weight").as_clustering() - self.assertMembershipsEqual(cl, [0,0,0,0,1,1]) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1]) self.assertAlmostEqual(cl.q, 0.1796, places=3) # Disconnected graph @@ -184,34 +212,34 @@ def testClauset(self): # Empty graph g = Graph(20) cl = g.community_fastgreedy().as_clustering() - self.assertMembershipsEqual(cl, range(g.vcount())) + self.assertMembershipsEqual(cl, list(range(g.vcount()))) def testEdgeBetweenness(self): # Full graph, no weights g = Graph.Full(5) cl = g.community_edge_betweenness().as_clustering() - self.assertMembershipsEqual(cl, [0]*5) + self.assertMembershipsEqual(cl, [0] * 5) # Full graph with weights g.es["weight"] = 1 - g[0,1] = g[1,2] = g[2,0] = g[3,4] = 10 + g[0, 1] = g[1, 2] = g[2, 0] = g[3, 4] = 10 # We need to specify the desired cluster count explicitly; this is # because edge betweenness-based detection does not play well with # modularity-based cluster count selection (the edge weights have # different semantics) so we need to give igraph a hint cl = g.community_edge_betweenness(weights="weight").as_clustering(n=2) - self.assertMembershipsEqual(cl, [0,0,0,1,1]) + self.assertMembershipsEqual(cl, [0, 0, 0, 1, 1]) self.assertAlmostEqual(cl.q, 0.2750, places=3) def testEigenvector(self): g = Graph.Full(5) + Graph.Full(5) g.add_edges([(0, 5)]) cl = g.community_leading_eigenvector() - self.assertMembershipsEqual(cl, [0,0,0,0,0,1,1,1,1,1]) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cl.q, 0.4523, places=3) cl = g.community_leading_eigenvector(2) - self.assertMembershipsEqual(cl, [0,0,0,0,0,1,1,1,1,1]) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) self.assertAlmostEqual(cl.q, 0.4523, places=3) def testInfomap(self): @@ -219,7 +247,11 @@ def testInfomap(self): cl = g.community_infomap() self.assertAlmostEqual(cl.codelength, 4.60605, places=3) self.assertAlmostEqual(cl.q, 0.40203, places=3) - self.assertMembershipsEqual(cl, [1,1,1,1,2,2,2,1,0,1,2,1,1,1,0,0,2,1,0,1,0,1] + [0]*12) + self.assertMembershipsEqual( + cl, + [1, 1, 1, 1, 2, 2, 2, 1, 0, 1, 2, 1, 1, 1, 0, 0, 2, 1, 0, 1, 0, 1] + + [0] * 12, + ) # Smoke testing with vertex and edge weights v_weights = [random.randint(1, 5) for _ in range(g.vcount())] @@ -234,29 +266,59 @@ def testLabelPropagation(self): # test. g = Graph.GRG(100, 0.2) cl = g.community_label_propagation() - g = Graph([(0,1),(1,2),(2,3)]) + g = Graph([(0, 1), (1, 2), (2, 3)]) g.es["weight"] = [2, 1, 2] g.vs["initial"] = [0, -1, -1, 1] - cl = g.community_label_propagation("weight", "initial", [1,0,0,1]) - self.assertMembershipsEqual(cl, [0,0,1,1]) - cl = g.community_label_propagation(initial="initial", fixed=[1,0,0,1]) - self.assertTrue(cl.membership == [0, 0, 1, 1] or \ - cl.membership == [0, 1, 1, 1] or \ - cl.membership == [0, 0, 0, 1]) + cl = g.community_label_propagation("weight", "initial", [1, 0, 0, 1]) + self.assertMembershipsEqual(cl, [0, 0, 1, 1]) + cl = g.community_label_propagation(initial="initial", fixed=[1, 0, 0, 1]) + self.assertTrue( + cl.membership == [0, 0, 1, 1] + or cl.membership == [0, 1, 1, 1] + or cl.membership == [0, 0, 0, 1] + ) def testMultilevel(self): # Example graph from the paper g = Graph(16) - g += [(0,2), (0,3), (0,4), (0,5), - (1,2), (1,4), (1,7), (2,4), (2,5), (2,6), - (3,7), (4,10), (5,7), (5,11), (6,7), (6,11), - (8,9), (8,10), (8,11), (8,14), (8,15), - (9,12), (9,14), (10,11), (10,12), (10,13), - (10,14), (11,13)] + g += [ + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (1, 2), + (1, 4), + (1, 7), + (2, 4), + (2, 5), + (2, 6), + (3, 7), + (4, 10), + (5, 7), + (5, 11), + (6, 7), + (6, 11), + (8, 9), + (8, 10), + (8, 11), + (8, 14), + (8, 15), + (9, 12), + (9, 14), + (10, 11), + (10, 12), + (10, 13), + (10, 14), + (11, 13), + ] cls = g.community_multilevel(return_levels=True) self.assertTrue(len(cls) == 2) - self.assertMembershipsEqual(cls[0], [1,1,1,0,1,1,0,0,2,2,2,3,2,3,2,2]) - self.assertMembershipsEqual(cls[1], [0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1]) + self.assertMembershipsEqual( + cls[0], [1, 1, 1, 0, 1, 1, 0, 0, 2, 2, 2, 3, 2, 3, 2, 2] + ) + self.assertMembershipsEqual( + cls[1], [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1] + ) self.assertAlmostEqual(cls[0].q, 0.346301, places=5) self.assertAlmostEqual(cls[1].q, 0.392219, places=5) @@ -271,21 +333,59 @@ def testOptimalModularity(self): ws = [i % 5 for i in range(g.ecount())] cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), - places=7) + self.assertAlmostEqual( + cl.q, g.modularity(cl.membership, weights=ws), places=7 + ) g = Graph.Famous("zachary") cl = g.community_optimal_modularity() self.assertTrue(len(cl) == 4) - self.assertMembershipsEqual(cl, [0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 1, \ - 0, 0, 0, 2, 2, 1, 0, 2, 0, 2, 0, 2, 3, 3, 3, 2, 3, 3, \ - 2, 2, 3, 2, 2]) + self.assertMembershipsEqual( + cl, + [ + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 0, + 2, + 2, + 1, + 0, + 0, + 0, + 2, + 2, + 1, + 0, + 2, + 0, + 2, + 0, + 2, + 3, + 3, + 3, + 2, + 3, + 3, + 2, + 2, + 3, + 2, + 2, + ], + ) self.assertAlmostEqual(cl.q, 0.4197896, places=7) - ws = [2+(i % 3) for i in range(g.ecount())] + ws = [2 + (i % 3) for i in range(g.ecount())] cl = g.community_optimal_modularity(weights=ws) - self.assertAlmostEqual(cl.q, g.modularity(cl.membership, weights=ws), - places=7) + self.assertAlmostEqual( + cl.q, g.modularity(cl.membership, weights=ws), places=7 + ) except NotImplementedError: # Well, meh @@ -293,51 +393,71 @@ def testOptimalModularity(self): def testSpinglass(self): g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) - g += [(0,5), (5,10), (10, 0)] + g += [(0, 5), (5, 10), (10, 0)] # Spinglass community detection is a bit unstable, so run it three times ok = False for i in range(3): cl = g.community_spinglass() - if self.reindexMembership(cl) == [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2]: + if self.reindexMembership(cl) == [ + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + ]: ok = True break self.assertTrue(ok) def testWalktrap(self): g = Graph.Full(5) + Graph.Full(5) + Graph.Full(5) - g += [(0,5), (5,10), (10, 0)] + g += [(0, 5), (5, 10), (10, 0)] cl = g.community_walktrap().as_clustering() - self.assertMembershipsEqual(cl, [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2]) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2]) cl = g.community_walktrap(steps=3).as_clustering() - self.assertMembershipsEqual(cl, [0,0,0,0,0,1,1,1,1,1,2,2,2,2,2]) + self.assertMembershipsEqual(cl, [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2]) def testLeiden(self): # Example from paper (Fig. C.1) high_weight = 3.0 - low_weight = 3.0/2.0 - edges = [(0, 1, high_weight), - (2, 3, high_weight), - (4, 2, high_weight), - (3, 4, high_weight), - (5, 6, high_weight), - (7, 5, high_weight), - (6, 7, high_weight), - (0, 2, low_weight), - (0, 3, low_weight), - (0, 4, low_weight), - (1, 5, low_weight), - (1, 6, low_weight), - (1, 7, low_weight)] + low_weight = 3.0 / 2.0 + edges = [ + (0, 1, high_weight), + (2, 3, high_weight), + (4, 2, high_weight), + (3, 4, high_weight), + (5, 6, high_weight), + (7, 5, high_weight), + (6, 7, high_weight), + (0, 2, low_weight), + (0, 3, low_weight), + (0, 4, low_weight), + (1, 5, low_weight), + (1, 6, low_weight), + (1, 7, low_weight), + ] G = Graph.TupleList(edges, weights=True) import random + random.seed(0) set_random_number_generator(random) # We don't find the optimal partition if we are greedy - cl = G.community_leiden("CPM", resolution_parameter=1, weights='weight', - beta=0, n_iterations=-1) + cl = G.community_leiden( + "CPM", resolution_parameter=1, weights="weight", beta=0, n_iterations=-1 + ) self.assertMembershipsEqual(cl, [0, 0, 1, 1, 1, 2, 2, 2]) random.seed(0) @@ -346,20 +466,27 @@ def testLeiden(self): # (The randomness is only present in the refinement, which is why we # start from all nodes in the same community: this should then be # refined). - cl = G.community_leiden("CPM", resolution_parameter=1, weights='weight', - beta=5, n_iterations=-1, - initial_membership=[0]*G.vcount()) + cl = G.community_leiden( + "CPM", + resolution_parameter=1, + weights="weight", + beta=5, + n_iterations=-1, + initial_membership=[0] * G.vcount(), + ) self.assertMembershipsEqual(cl, [0, 1, 0, 0, 0, 1, 1, 1]) + class CohesiveBlocksTests(unittest.TestCase): def genericTests(self, cbs): self.assertTrue(isinstance(cbs, CohesiveBlocks)) - self.assertTrue(all(cbs.cohesion(i) == c - for i, c in enumerate(cbs.cohesions()))) - self.assertTrue(all(cbs.parent(i) == c - for i, c in enumerate(cbs.parents()))) - self.assertTrue(all(cbs.max_cohesion(i) == c - for i, c in enumerate(cbs.max_cohesions()))) + self.assertTrue( + all(cbs.cohesion(i) == c for i, c in enumerate(cbs.cohesions())) + ) + self.assertTrue(all(cbs.parent(i) == c for i, c in enumerate(cbs.parents()))) + self.assertTrue( + all(cbs.max_cohesion(i) == c for i, c in enumerate(cbs.max_cohesions())) + ) def testCohesiveBlocks1(self): # Taken from the igraph R manual @@ -369,33 +496,52 @@ def testCohesiveBlocks1(self): cbs = g.cohesive_blocks() self.genericTests(cbs) - self.assertEqual(sorted(list(cbs)), - [list(range(0, 5)), list(range(18)), [0, 1, 2, 3, 4, 6, 7, 8, 9, 10], - list(range(6, 10)), list(range(12, 16)), list(range(12, 17))]) + self.assertEqual( + sorted(list(cbs)), + [ + list(range(0, 5)), + list(range(18)), + [0, 1, 2, 3, 4, 6, 7, 8, 9, 10], + list(range(6, 10)), + list(range(12, 16)), + list(range(12, 17)), + ], + ) self.assertEqual(cbs.cohesions(), [1, 2, 2, 4, 3, 3]) - self.assertEqual(cbs.max_cohesions(), [4, 4, 4, 4, 4, - 1, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 2, 1]) + self.assertEqual( + cbs.max_cohesions(), [4, 4, 4, 4, 4, 1, 3, 3, 3, 3, 2, 1, 3, 3, 3, 3, 2, 1] + ) self.assertEqual(cbs.parents(), [None, 0, 0, 1, 2, 1]) def testCohesiveBlocks2(self): # Taken from the Moody-White paper - g = Graph.Formula("1-2:3:4:5:6, 2-3:4:5:7, 3-4:6:7, 4-5:6:7, " - "5-6:7:21, 6-7, 7-8:11:14:19, 8-9:11:14, 9-10, " - "10-12:13, 11-12:14, 12-16, 13-16, 14-15, 15-16, " - "17-18:19:20, 18-20:21, 19-20:22:23, 20-21, " - "21-22:23, 22-23") + g = Graph.Formula( + "1-2:3:4:5:6, 2-3:4:5:7, 3-4:6:7, 4-5:6:7, " + "5-6:7:21, 6-7, 7-8:11:14:19, 8-9:11:14, 9-10, " + "10-12:13, 11-12:14, 12-16, 13-16, 14-15, 15-16, " + "17-18:19:20, 18-20:21, 19-20:22:23, 20-21, " + "21-22:23, 22-23" + ) cbs = g.cohesive_blocks() self.genericTests(cbs) - expected_blocks = [list(range(7)), list(range(23)), list(range(7))+list(range(16, 23)), - list(range(6, 16)), [6, 7, 10, 13]] - observed_blocks = sorted(sorted(int(x)-1 for x in g.vs[bl]["name"]) for bl in cbs) + expected_blocks = [ + list(range(7)), + list(range(23)), + list(range(7)) + list(range(16, 23)), + list(range(6, 16)), + [6, 7, 10, 13], + ] + observed_blocks = sorted( + sorted(int(x) - 1 for x in g.vs[bl]["name"]) for bl in cbs + ) self.assertEqual(expected_blocks, observed_blocks) self.assertTrue(cbs.cohesions() == [1, 2, 2, 5, 3]) self.assertTrue(cbs.parents() == [None, 0, 0, 1, 2]) - self.assertTrue(sorted(cbs.hierarchy().get_edgelist()) == - [(0, 1), (0, 2), (1, 3), (2, 4)]) + self.assertTrue( + sorted(cbs.hierarchy().get_edgelist()) == [(0, 1), (0, 2), (1, 3), (2, 4)] + ) def testCohesiveBlockingErrors(self): g = Graph.GRG(100, 0.2) @@ -409,14 +555,17 @@ def setUp(self): ([0, 0, 0, 1, 1, 1], [1, 1, 1, 0, 0, 0]), ([0, 0, 0, 1, 1, 1], [0, 0, 1, 1, 2, 2]), ([0, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5]), - ([0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2], - [2, 0, 1, 0, 2, 0, 2, 0, 1, 0, 3, 1]) + ( + [0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 2], + [2, 0, 1, 0, 2, 0, 2, 0, 1, 0, 3, 1], + ), ] def _testMethod(self, method, expected): for clusters, result in zip(self.clusterings, expected): - self.assertAlmostEqual(compare_communities(method=method, *clusters), - result, places=3) + self.assertAlmostEqual( + compare_communities(method=method, *clusters), result, places=3 + ) def testCompareVI(self): expected = [0, 0.8675, math.log(6)] @@ -435,7 +584,7 @@ def testCompareSplitJoin(self): self.assertEqual(split_join_distance(l1, l2), (6, 5)) def testCompareRand(self): - expected = [1, 2/3., 0, 0.590909] + expected = [1, 2 / 3.0, 0, 0.590909] self._testMethod("rand", expected) def testCompareAdjustedRand(self): @@ -444,9 +593,11 @@ def testCompareAdjustedRand(self): def testRemoveNone(self): l1 = Clustering([1, 1, 1, None, None, 2, 2, 2, 2]) - l2 = Clustering([1, 1, 2, 2, None, 2, 3, 3, None]) - self.assertAlmostEqual(compare_communities(l1, l2, "nmi", remove_none=True), \ - 0.5158, places=3) + l2 = Clustering([1, 1, 2, 2, None, 2, 3, 3, None]) + self.assertAlmostEqual( + compare_communities(l1, l2, "nmi", remove_none=True), 0.5158, places=3 + ) + def suite(): decomposition_suite = unittest.makeSuite(DecompositionTests) @@ -456,14 +607,23 @@ def suite(): community_suite = unittest.makeSuite(CommunityTests) cohesive_blocks_suite = unittest.makeSuite(CohesiveBlocksTests) comparison_suite = unittest.makeSuite(ComparisonTests) - return unittest.TestSuite([decomposition_suite, clustering_suite, \ - vertex_clustering_suite, cover_suite, community_suite, \ - cohesive_blocks_suite, comparison_suite]) + return unittest.TestSuite( + [ + decomposition_suite, + clustering_suite, + vertex_clustering_suite, + cover_suite, + community_suite, + cohesive_blocks_suite, + comparison_suite, + ] + ) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_edgeseq.py b/tests/test_edgeseq.py index d5235882d..6ec581d70 100644 --- a/tests/test_edgeseq.py +++ b/tests/test_edgeseq.py @@ -52,7 +52,7 @@ def testRepr(self): output = repr(self.g.es[0]) self.assertEqual(output, "igraph.Edge(%r, 0, {})" % self.g) - self.g.es["weight"] = range(10, 0, -1) + self.g.es["weight"] = list(range(10, 0, -1)) output = repr(self.g.es[3]) self.assertEqual(output, "igraph.Edge(%r, 3, {'weight': 7})" % self.g) @@ -128,7 +128,7 @@ def assert_edges_unique_in(self, es): def setUp(self): self.g = Graph.Full(10) - self.g.es["test"] = range(45) + self.g.es["test"] = list(range(45)) def testCreation(self): self.assertTrue(len(EdgeSeq(self.g)) == 45) @@ -176,7 +176,7 @@ def testPartialAttributeAssignment(self): expected = [[0, i][i % 2] for i in range(self.g.ecount())] self.assertTrue(self.g.es["test"] == expected) - only_even["test2"] = range(23) + only_even["test2"] = list(range(23)) expected = [[i // 2, None][i % 2] for i in range(self.g.ecount())] self.assertTrue(self.g.es["test2"] == expected) @@ -255,7 +255,7 @@ def testIntegerFilteringSelect(self): self.assertTrue(subset["test"] == [2, 3, 4, 2]) def testIterableFilteringSelect(self): - subset = self.g.es.select(range(5, 8)) + subset = self.g.es.select(list(range(5, 8))) self.assertTrue(len(subset) == 3) self.assertTrue(subset["test"] == [5, 6, 7]) diff --git a/tests/test_flow.py b/tests/test_flow.py index b420c8995..e2319a4b2 100644 --- a/tests/test_flow.py +++ b/tests/test_flow.py @@ -4,6 +4,7 @@ from itertools import combinations from random import randint + class MaxFlowTests(unittest.TestCase): def setUp(self): self.g = Graph(4, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)]) @@ -11,8 +12,7 @@ def setUp(self): self.g.es["capacity"] = self.capacities def testCapacities(self): - self.assertTrue(self.capacities == \ - self.g.es.get_attribute_values("capacity")) + self.assertTrue(self.capacities == self.g.es.get_attribute_values("capacity")) def testEdgeConnectivity(self): self.assertTrue(self.g.edge_connectivity(0, 3) == 2) @@ -48,8 +48,10 @@ def testMaxFlow(self): self.assertEqual(flow.value, 4) self.assertEqual(flow.cut, [3, 4]) self.assertEqual([e.index for e in flow.es], [3, 4]) - self.assertTrue(set(flow.partition[0]).union(flow.partition[1]) == \ - set(range(self.g.vcount()))) + self.assertTrue( + set(flow.partition[0]).union(flow.partition[1]) + == set(range(self.g.vcount())) + ) self.assertRaises(KeyError, self.g.maxflow, 0, 3, "unknown") @@ -61,9 +63,9 @@ def constructSimpleGraph(self, directed=False): return g def constructLadderGraph(self, directed=False): - el = list(zip(range(0, 5), range(1, 6))) - el += list(zip(range(6, 11), range(7, 12))) - el += list(zip(range(0, 6), range(6, 12))) + el = list(zip(list(range(0, 5)), list(range(1, 6)))) + el += list(zip(list(range(6, 11)), list(range(7, 12)))) + el += list(zip(list(range(0, 6)), list(range(6, 12)))) g = Graph(el, directed=directed) return g @@ -82,8 +84,9 @@ def testMinCut(self): mc = g.mincut() self.assertTrue(isinstance(mc, Cut)) self.assertTrue(mc.value == 2) - self.assertTrue(set(mc.partition[0]).union(mc.partition[1]) == \ - set(range(g.vcount()))) + self.assertTrue( + set(mc.partition[0]).union(mc.partition[1]) == set(range(g.vcount())) + ) self.assertTrue(isinstance(str(mc), str)) self.assertTrue(isinstance(repr(mc), str)) self.assertTrue(isinstance(mc.es, EdgeSeq)) @@ -98,8 +101,9 @@ def testMinCutWithSourceAndTarget(self): self.assertTrue(isinstance(mc, Cut)) self.assertTrue(mc.cut == [3, 4]) self.assertTrue(mc.value == 4) - self.assertTrue(set(mc.partition[0]).union(mc.partition[1]) == \ - set(range(g.vcount()))) + self.assertTrue( + set(mc.partition[0]).union(mc.partition[1]) == set(range(g.vcount())) + ) mc = g.mincut(0, 3) self.assertTrue(isinstance(mc, Cut)) self.assertTrue(mc.cut == [3, 4]) @@ -116,8 +120,9 @@ def testStMinCut(self): self.assertTrue(isinstance(mc, Cut)) self.assertTrue(mc.cut == [3, 4]) self.assertTrue(mc.value == 4) - self.assertTrue(set(mc.partition[0]).union(mc.partition[1]) == \ - set(range(g.vcount()))) + self.assertTrue( + set(mc.partition[0]).union(mc.partition[1]) == set(range(g.vcount())) + ) mc = g.st_mincut(0, 3) self.assertTrue(isinstance(mc, Cut)) self.assertTrue(mc.cut == [3, 4]) @@ -128,23 +133,29 @@ def testStMinCut(self): self.assertTrue(mc.value == 6) self.assertRaises(KeyError, g.st_mincut, 2, 0, capacity="unknown") - def testAllSTCuts1(self): # Simple graph with four vertices g = self.constructSimpleGraph(directed=True) - partitions = [((0, 1, 1, 1), 2), ((0, 0, 1, 1), 3), - ((0, 1, 0, 1), 2), ((0, 0, 0, 1), 2)] + partitions = [ + ((0, 1, 1, 1), 2), + ((0, 0, 1, 1), 3), + ((0, 1, 0, 1), 2), + ((0, 0, 0, 1), 2), + ] values = dict(partitions) partitions = [partition for partition, _ in partitions] - for cut in g.all_st_cuts(0,3): + for cut in g.all_st_cuts(0, 3): membership = tuple(cut.membership) - self.assertTrue(membership in partitions, - "%r not found among expected partitions" % (membership,)) + self.assertTrue( + membership in partitions, + "%r not found among expected partitions" % (membership,), + ) self.assertEqual(cut.value, values[membership]) self.assertEqual(len(cut.es), values[membership]) partitions.remove(membership) - self.assertTrue(partitions == [], - "expected partitions not seen: %r" % (partitions, )) + self.assertTrue( + partitions == [], "expected partitions not seen: %r" % (partitions,) + ) def testAllSTCuts2(self): # "Ladder graph" @@ -155,11 +166,11 @@ def testAllSTCuts2(self): for cut in cuts: g2 = g.copy() g2.delete_edges(cut.es) - self.assertFalse(g2.is_connected(), - "%r is not a real cut" % (cut.membership,)) + self.assertFalse( + g2.is_connected(), "%r is not a real cut" % (cut.membership,) + ) self.assertFalse(cut.value < 2 or cut.value > 6) - def testAllSTMinCuts2(self): # "Ladder graph" g = self.constructLadderGraph() @@ -171,14 +182,15 @@ def testAllSTMinCuts2(self): self.assertEqual(cut.value, 2) g2 = g.copy() g2.delete_edges(cut.es) - self.assertFalse(g2.is_connected(), - "%r is not a real cut" % (cut.membership,)) + self.assertFalse( + g2.is_connected(), "%r is not a real cut" % (cut.membership,) + ) g.es["capacity"] = [2, 1, 2, 1, 2, 2, 1, 2, 1, 2, 1, 1, 1, 1, 1] cuts = g.all_st_mincuts(0, 11, "capacity") self.assertEqual(len(cuts), 2) - self.assertEqual(cuts[0].membership, [0,0,1,1,1,1,0,0,1,1,1,1]) - self.assertEqual(cuts[1].membership, [0,0,0,0,1,1,0,0,0,0,1,1]) + self.assertEqual(cuts[0].membership, [0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1]) + self.assertEqual(cuts[1].membership, [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1]) self.assertEqual(cuts[0].value, 2) self.assertEqual(cuts[1].value, 2) @@ -191,16 +203,22 @@ def testEmpty(self): self.assertEqual(0, t.ecount()) def testSimpleExample(self): - g = Graph(6, [(0,1),(0,2),(1,2),(1,3),(1,4),(2,4),(3,4),(3,5),(4,5)], \ - directed=False) - g.es["capacity"] = [1,7,1,3,2,4,1,6,2] + g = Graph( + 6, + [(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4), (3, 5), (4, 5)], + directed=False, + ) + g.es["capacity"] = [1, 7, 1, 3, 2, 4, 1, 6, 2] t = g.gomory_hu_tree("capacity") self.validate_gomory_hu_tree(g, t) def testDirected(self): - g = Graph(6, [(0,1),(0,2),(1,2),(1,3),(1,4),(2,4),(3,4),(3,5),(4,5)], \ - directed=True) - g.es["capacity"] = [1,7,1,3,2,4,1,6,2] + g = Graph( + 6, + [(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4), (3, 5), (4, 5)], + directed=True, + ) + g.es["capacity"] = [1, 7, 1, 3, 2, 4, 1, 6, 2] self.assertRaises(InternalError, g.gomory_hu_tree, "capacity") def testRandomGRG(self): @@ -213,7 +231,7 @@ def validate_gomory_hu_tree(self, g, t): n = g.vcount() self.assertEqual(n, t.vcount()) - self.assertEqual(n-1, t.ecount()) + self.assertEqual(n - 1, t.ecount()) self.assertFalse(t.is_directed()) if "capacity" in g.edge_attributes(): @@ -221,7 +239,7 @@ def validate_gomory_hu_tree(self, g, t): else: capacities = None - for i, j in combinations(range(n), 2): + for i, j in combinations(list(range(n)), 2): path = t.get_shortest_paths(i, j, output="epath") if path: path = path[0] @@ -229,16 +247,18 @@ def validate_gomory_hu_tree(self, g, t): observed_flow = g.maxflow_value(i, j, capacities) self.assertEqual(observed_flow, expected_flow) + def suite(): flow_suite = unittest.makeSuite(MaxFlowTests) cut_suite = unittest.makeSuite(CutTests) gomory_hu_suite = unittest.makeSuite(GomoryHuTests) return unittest.TestSuite([flow_suite, cut_suite, gomory_hu_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_foreign.py b/tests/test_foreign.py index ff30ec2af..8f2ace5d0 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import io import unittest import warnings @@ -11,7 +9,8 @@ class ForeignTests(unittest.TestCase): def testDIMACS(self): - with temporary_file(u"""\ + with temporary_file( + """\ c c This is a simple example file to demonstrate the c DIMACS input file format for minimum-cost flow problems. @@ -29,17 +28,18 @@ def testDIMACS(self): a 2 3 2 a 2 4 3 a 3 4 5 - """) as tmpfname: + """ + ) as tmpfname: graph = Graph.Read_DIMACS(tmpfname, False) self.assertTrue(isinstance(graph, Graph)) self.assertTrue(graph.vcount() == 4 and graph.ecount() == 5) self.assertTrue(graph["source"] == 0 and graph["target"] == 3) - self.assertTrue(graph.es["capacity"] == [4,2,2,3,5]) + self.assertTrue(graph.es["capacity"] == [4, 2, 2, 3, 5]) graph.write_dimacs(tmpfname) - def testDL(self): - with temporary_file(u"""\ + with temporary_file( + """\ dl n=5 format = fullmatrix labels embedded @@ -50,16 +50,32 @@ def testDL(self): Lin 1 0 0 1 0 Pat 1 0 1 0 1 russ 0 1 0 1 0 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_DL(tmpfname) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 5 and g.ecount() == 12) self.assertTrue(g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(0,3),(1,0),(1,4),(2,0),(2,3),(3,0),\ - (3,2),(3,4),(4,1),(4,3)]) + self.assertTrue( + sorted(g.get_edgelist()) + == [ + (0, 1), + (0, 2), + (0, 3), + (1, 0), + (1, 4), + (2, 0), + (2, 3), + (3, 0), + (3, 2), + (3, 4), + (4, 1), + (4, 3), + ] + ) - with temporary_file(u"""\ + with temporary_file( + """\ dl n=5 format = fullmatrix labels: @@ -72,16 +88,32 @@ def testDL(self): 1 0 0 1 0 1 0 1 0 1 0 1 0 1 0 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_DL(tmpfname) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 5 and g.ecount() == 12) self.assertTrue(g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(0,3),(1,0),(1,4),(2,0),(2,3),(3,0),\ - (3,2),(3,4),(4,1),(4,3)]) + self.assertTrue( + sorted(g.get_edgelist()) + == [ + (0, 1), + (0, 2), + (0, 3), + (1, 0), + (1, 4), + (2, 0), + (2, 3), + (3, 0), + (3, 2), + (3, 4), + (4, 1), + (4, 3), + ] + ) - with temporary_file(u"""\ + with temporary_file( + """\ DL n=5 format = edgelist1 labels: @@ -93,60 +125,70 @@ def testDL(self): sally jim 4 billy george 5 jane jim 6 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_DL(tmpfname, False) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 5 and g.ecount() == 5) self.assertTrue(not g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(0,3),(1,2),(2,4)]) + self.assertTrue( + sorted(g.get_edgelist()) == [(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)] + ) def _testNCOLOrLGL(self, func, fname, can_be_reopened=True): - g = func(fname, names=False, weights=False, \ - directed=False) + g = func(fname, names=False, weights=False, directed=False) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 4 and g.ecount() == 5) self.assertTrue(not g.is_directed()) - self.assertTrue(sorted(g.get_edgelist()) == \ - [(0,1),(0,2),(1,1),(1,3),(2,3)]) - self.assertTrue("name" not in g.vertex_attributes() and \ - "weight" not in g.edge_attributes()) + self.assertTrue( + sorted(g.get_edgelist()) == [(0, 1), (0, 2), (1, 1), (1, 3), (2, 3)] + ) + self.assertTrue( + "name" not in g.vertex_attributes() and "weight" not in g.edge_attributes() + ) if not can_be_reopened: return - g = func(fname, names=False, \ - directed=False) - self.assertTrue("name" not in g.vertex_attributes() and \ - "weight" in g.edge_attributes()) + g = func(fname, names=False, directed=False) + self.assertTrue( + "name" not in g.vertex_attributes() and "weight" in g.edge_attributes() + ) self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) g = func(fname, directed=False) - self.assertTrue("name" in g.vertex_attributes() and \ - "weight" in g.edge_attributes()) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" in g.edge_attributes() + ) self.assertTrue(g.vs["name"] == ["eggs", "spam", "ham", "bacon"]) self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) def testNCOL(self): - with temporary_file(u"""\ + with temporary_file( + """\ eggs spam 1 ham eggs 2 ham bacon bacon spam 3 - spam spam""") as tmpfname: + spam spam""" + ) as tmpfname: self._testNCOLOrLGL(func=Graph.Read_Ncol, fname=tmpfname) - with temporary_file(u"""\ + with temporary_file( + """\ eggs spam ham eggs ham bacon bacon spam - spam spam""") as tmpfname: + spam spam""" + ) as tmpfname: g = Graph.Read_Ncol(tmpfname) - self.assertTrue("name" in g.vertex_attributes() and \ - "weight" not in g.edge_attributes()) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() + ) def testLGL(self): - with temporary_file(u"""\ + with temporary_file( + """\ # eggs spam 1 # ham @@ -155,10 +197,12 @@ def testLGL(self): # bacon spam 3 # spam - spam""") as tmpfname: + spam""" + ) as tmpfname: self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=tmpfname) - with temporary_file(u"""\ + with temporary_file( + """\ # eggs spam # ham @@ -167,23 +211,28 @@ def testLGL(self): # bacon spam # spam - spam""") as tmpfname: + spam""" + ) as tmpfname: with warnings.catch_warnings(): warnings.simplefilter("ignore") g = Graph.Read_Lgl(tmpfname) - self.assertTrue("name" in g.vertex_attributes() and \ - "weight" not in g.edge_attributes()) + self.assertTrue( + "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() + ) # This is not an LGL file; we are testing error handling here - with temporary_file(u"""\ + with temporary_file( + """\ 1 2 1 3 - """) as tmpfname: + """ + ) as tmpfname: with self.assertRaises(InternalError): Graph.Read_Lgl(tmpfname) def testLGLWithIOModule(self): - with temporary_file(u"""\ + with temporary_file( + """\ # eggs spam 1 # ham @@ -192,13 +241,16 @@ def testLGLWithIOModule(self): # bacon spam 3 # spam - spam""") as tmpfname: + spam""" + ) as tmpfname: with io.open(tmpfname, "r") as fp: - self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=fp, - can_be_reopened=False) + self._testNCOLOrLGL( + func=Graph.Read_Lgl, fname=fp, can_be_reopened=False + ) def testAdjacency(self): - with temporary_file(u"""\ + with temporary_file( + """\ # Test comment line 0 1 1 0 0 0 1 0 1 0 0 0 @@ -206,22 +258,73 @@ def testAdjacency(self): 0 0 0 0 2 2 0 0 0 2 0 2 0 0 0 2 2 0 - """) as tmpfname: + """ + ) as tmpfname: g = Graph.Read_Adjacency(tmpfname) self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 6 and g.ecount() == 18 and - g.is_directed() and "weight" not in g.edge_attributes()) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 18 + and g.is_directed() + and "weight" not in g.edge_attributes() + ) g = Graph.Read_Adjacency(tmpfname, attribute="weight") self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 6 and g.ecount() == 12 and - g.is_directed() and g.es["weight"] == [1,1,1,1,1,1,2,2,2,2,2,2]) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 12 + and g.is_directed() + and g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2] + ) g.write_adjacency(tmpfname) def testPickle(self): - pickle = [128, 2, 99, 105, 103, 114, 97, 112, 104, 10, 71, 114, 97, 112, - 104, 10, 113, 1, 40, 75, 3, 93, 113, 2, 75, 1, 75, 2, 134, 113, 3, 97, - 137, 125, 125, 125, 116, 82, 113, 4, 125, 98, 46] + pickle = [ + 128, + 2, + 99, + 105, + 103, + 114, + 97, + 112, + 104, + 10, + 71, + 114, + 97, + 112, + 104, + 10, + 113, + 1, + 40, + 75, + 3, + 93, + 113, + 2, + 75, + 1, + 75, + 2, + 134, + 113, + 3, + 97, + 137, + 125, + 125, + 125, + 116, + 82, + 113, + 4, + 125, + 98, + 46, + ] if sys.version_info > (3, 0): pickle = bytes(pickle) else: @@ -229,8 +332,7 @@ def testPickle(self): with temporary_file(pickle, "wb", binary=True) as tmpfname: g = Graph.Read_Pickle(pickle) self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and - not g.is_directed()) + self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) @@ -238,10 +340,11 @@ def suite(): foreign_suite = unittest.makeSuite(ForeignTests) return unittest.TestSuite([foreign_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_games.py b/tests/test_games.py index 07bc62eb2..0af5b9aa2 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -1,6 +1,7 @@ import unittest from igraph import * + class GameTests(unittest.TestCase): def testGRG(self): g = Graph.GRG(50, 0.2) @@ -9,44 +10,44 @@ def testGRG(self): self.assertTrue(isinstance(g, Graph)) self.assertTrue("x" in g.vertex_attributes()) self.assertTrue("y" in g.vertex_attributes()) - self.assertTrue(isinstance(Layout(zip(g.vs["x"], g.vs["y"])), Layout)) + self.assertTrue(isinstance(Layout(list(zip(g.vs["x"], g.vs["y"]))), Layout)) def testForestFire(self): - g=Graph.Forest_Fire(100, 0.1) - self.assertTrue(isinstance(g, Graph) and g.is_directed() == False) - g=Graph.Forest_Fire(100, 0.1, directed=True) - self.assertTrue(isinstance(g, Graph) and g.is_directed() == True) + g = Graph.Forest_Fire(100, 0.1) + self.assertTrue(isinstance(g, Graph) and g.is_directed() is False) + g = Graph.Forest_Fire(100, 0.1, directed=True) + self.assertTrue(isinstance(g, Graph) and g.is_directed() is True) def testRecentDegree(self): - g=Graph.Recent_Degree(100, 5, 10) + g = Graph.Recent_Degree(100, 5, 10) self.assertTrue(isinstance(g, Graph)) def testPreference(self): - g=Graph.Preference(100, [1, 1], [[1, 0], [0, 1]]) + g = Graph.Preference(100, [1, 1], [[1, 0], [0, 1]]) self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 2) - g=Graph.Preference(100, [1, 1], [[1, 0], [0, 1]], attribute="type") - l=g.vs.get_attribute_values("type") + g = Graph.Preference(100, [1, 1], [[1, 0], [0, 1]], attribute="type") + l = g.vs.get_attribute_values("type") self.assertTrue(min(l) == 0 and max(l) == 1) def testAsymmetricPreference(self): - g=Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[0, 1], [1, 0]]) + g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[0, 1], [1, 0]]) self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 2) - g=Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]],\ - attribute="type") - l=g.vs.get_attribute_values("type") - l1=[i[0] for i in l] - l2=[i[1] for i in l] - self.assertTrue(min(l1) == 0 and max(l1) == 1 and - min(l2) == 0 and max(l2) == 1) + g = Graph.Asymmetric_Preference( + 100, [[0, 1], [1, 0]], [[1, 0], [0, 1]], attribute="type" + ) + l = g.vs.get_attribute_values("type") + l1 = [i[0] for i in l] + l2 = [i[1] for i in l] + self.assertTrue(min(l1) == 0 and max(l1) == 1 and min(l2) == 0 and max(l2) == 1) - g=Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]]) + g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]]) self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 1) def testWattsStrogatz(self): - g=Graph.Watts_Strogatz(1, 20, 1, 0.2) - self.assertTrue(isinstance(g, Graph) and g.vcount()==20 and g.ecount()==20) + g = Graph.Watts_Strogatz(1, 20, 1, 0.2) + self.assertTrue(isinstance(g, Graph) and g.vcount() == 20 and g.ecount() == 20) def testRandomBipartiteNP(self): # Test np mode, undirected @@ -54,14 +55,14 @@ def testRandomBipartiteNP(self): self.assertTrue(g.is_simple()) self.assertTrue(g.is_bipartite()) self.assertFalse(g.is_directed()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) # Test np mode, directed, "out" g = Graph.Random_Bipartite(10, 20, p=0.25, directed=True, neimode="out") self.assertTrue(g.is_simple()) self.assertTrue(g.is_bipartite()) self.assertTrue(g.is_directed()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) self.assertTrue(all(g.vs[e.tuple]["type"] == [False, True] for e in g.es)) # Test np mode, directed, "in" @@ -69,7 +70,7 @@ def testRandomBipartiteNP(self): self.assertTrue(g.is_simple()) self.assertTrue(g.is_bipartite()) self.assertTrue(g.is_directed()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) self.assertTrue(all(g.vs[e.tuple]["type"] == [True, False] for e in g.es)) # Test np mode, directed, "all" @@ -77,7 +78,7 @@ def testRandomBipartiteNP(self): self.assertTrue(g.is_simple()) self.assertTrue(g.is_bipartite()) self.assertTrue(g.is_directed()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) def testRandomBipartiteNM(self): # Test np mode, undirected @@ -86,7 +87,7 @@ def testRandomBipartiteNM(self): self.assertTrue(g.is_bipartite()) self.assertFalse(g.is_directed()) self.assertEqual(50, g.ecount()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) # Test np mode, directed, "out" g = Graph.Random_Bipartite(10, 20, m=50, directed=True, neimode="out") @@ -94,7 +95,7 @@ def testRandomBipartiteNM(self): self.assertTrue(g.is_bipartite()) self.assertTrue(g.is_directed()) self.assertEqual(50, g.ecount()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) self.assertTrue(all(g.vs[e.tuple]["type"] == [False, True] for e in g.es)) # Test np mode, directed, "in" @@ -103,7 +104,7 @@ def testRandomBipartiteNM(self): self.assertTrue(g.is_bipartite()) self.assertTrue(g.is_directed()) self.assertEqual(50, g.ecount()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) self.assertTrue(all(g.vs[e.tuple]["type"] == [True, False] for e in g.es)) # Test np mode, directed, "all" @@ -112,12 +113,12 @@ def testRandomBipartiteNM(self): self.assertTrue(g.is_bipartite()) self.assertTrue(g.is_directed()) self.assertEqual(50, g.ecount()) - self.assertEqual([False]*10 + [True]*20, g.vs["type"]) + self.assertEqual([False] * 10 + [True] * 20, g.vs["type"]) def testRewire(self): # Undirected graph - g=Graph.GRG(25, 0.4) - degrees=g.degree() + g = Graph.GRG(25, 0.4) + degrees = g.degree() # Rewiring without loops g.rewire(10000) @@ -131,14 +132,14 @@ def testRewire(self): # Rewiring with loops (2) g = Graph.Full(4) - g[1,3] = 0 + g[1, 3] = 0 degrees = g.degree() g.rewire(100, mode="loops") self.assertEqual(degrees, g.degree()) self.assertFalse(any(g.is_multiple())) # Directed graph - g=Graph.GRG(25, 0.4) + g = Graph.GRG(25, 0.4) g.to_directed("mutual") indeg, outdeg = g.indegree(), g.outdegree() g.rewire(10000) @@ -152,14 +153,16 @@ def testRewire(self): self.assertEqual(outdeg, g.outdegree()) self.assertFalse(any(g.is_multiple())) + def suite(): game_suite = unittest.makeSuite(GameTests) return unittest.TestSuite([game_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_generators.py b/tests/test_generators.py index e29e3a079..f2d26c75a 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -11,25 +11,25 @@ class GeneratorTests(unittest.TestCase): def testStar(self): - g=Graph.Star(5, "in") - el=[(1,0),(2,0),(3,0),(4,0)] + g = Graph.Star(5, "in") + el = [(1, 0), (2, 0), (3, 0), (4, 0)] self.assertTrue(g.is_directed()) self.assertTrue(g.get_edgelist() == el) - g=Graph.Star(5, "out", center=2) - el=[(2,0),(2,1),(2,3),(2,4)] + g = Graph.Star(5, "out", center=2) + el = [(2, 0), (2, 1), (2, 3), (2, 4)] self.assertTrue(g.is_directed()) self.assertTrue(g.get_edgelist() == el) - g=Graph.Star(5, "mutual", center=2) - el=[(0,2),(1,2),(2,0),(2,1),(2,3),(2,4),(3,2),(4,2)] + g = Graph.Star(5, "mutual", center=2) + el = [(0, 2), (1, 2), (2, 0), (2, 1), (2, 3), (2, 4), (3, 2), (4, 2)] self.assertTrue(g.is_directed()) self.assertTrue(sorted(g.get_edgelist()) == el) - g=Graph.Star(5, center=3) - el=[(0,3),(1,3),(2,3),(3,4)] + g = Graph.Star(5, center=3) + el = [(0, 3), (1, 3), (2, 3), (3, 4)] self.assertTrue(not g.is_directed()) self.assertTrue(sorted(g.get_edgelist()) == el) def testFamous(self): - g=Graph.Famous("tutte") + g = Graph.Famous("tutte") self.assertTrue(g.vcount() == 46 and g.ecount() == 69) self.assertRaises(InternalError, Graph.Famous, "unknown") @@ -40,43 +40,56 @@ def testFormula(self): ("A", ["A"], []), ("A-B", ["A", "B"], [(0, 1)]), ("A --- B", ["A", "B"], [(0, 1)]), - ("A--B, C--D, E--F, G--H, I, J, K", + ( + "A--B, C--D, E--F, G--H, I, J, K", ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"], - [(0,1), (2,3), (4,5), (6,7)] + [(0, 1), (2, 3), (4, 5), (6, 7)], ), - ("A:B:C:D -- A:B:C:D", + ( + "A:B:C:D -- A:B:C:D", ["A", "B", "C", "D"], - [(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)] + [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], ), - ("A -> B -> C", ["A", "B", "C"], [(0,1), (1,2)]), - ("A <- B -> C", ["A", "B", "C"], [(1,0), (1,2)]), - ("A <- B -- C", ["A", "B", "C"], [(1,0)]), - ("A <-> B <---> C <> D", ["A", "B", "C", "D"], - [(0,1), (1,0), (1,2), (2,1), (2,3), (3,2)]), - ("'this is' <- 'a silly' -> 'graph here'", - ["this is", "a silly", "graph here"], [(1,0), (1,2)]), - ("Alice-Bob-Cecil-Alice, Daniel-Cecil-Eugene, Cecil-Gordon", + ("A -> B -> C", ["A", "B", "C"], [(0, 1), (1, 2)]), + ("A <- B -> C", ["A", "B", "C"], [(1, 0), (1, 2)]), + ("A <- B -- C", ["A", "B", "C"], [(1, 0)]), + ( + "A <-> B <---> C <> D", + ["A", "B", "C", "D"], + [(0, 1), (1, 0), (1, 2), (2, 1), (2, 3), (3, 2)], + ), + ( + "'this is' <- 'a silly' -> 'graph here'", + ["this is", "a silly", "graph here"], + [(1, 0), (1, 2)], + ), + ( + "Alice-Bob-Cecil-Alice, Daniel-Cecil-Eugene, Cecil-Gordon", ["Alice", "Bob", "Cecil", "Daniel", "Eugene", "Gordon"], - [(0,1),(1,2),(0,2),(2,3),(2,4),(2,5)] + [(0, 1), (1, 2), (0, 2), (2, 3), (2, 4), (2, 5)], ), - ("Alice-Bob:Cecil:Daniel, Cecil:Daniel-Eugene:Gordon", + ( + "Alice-Bob:Cecil:Daniel, Cecil:Daniel-Eugene:Gordon", ["Alice", "Bob", "Cecil", "Daniel", "Eugene", "Gordon"], - [(0,1),(0,2),(0,3),(2,4),(2,5),(3,4),(3,5)] + [(0, 1), (0, 2), (0, 3), (2, 4), (2, 5), (3, 4), (3, 5)], ), - ("Alice <-> Bob --> Cecil <-- Daniel, Eugene --> Gordon:Helen", + ( + "Alice <-> Bob --> Cecil <-- Daniel, Eugene --> Gordon:Helen", ["Alice", "Bob", "Cecil", "Daniel", "Eugene", "Gordon", "Helen"], - [(0,1),(1,0),(1,2),(3,2),(4,5),(4,6)] + [(0, 1), (1, 0), (1, 2), (3, 2), (4, 5), (4, 6)], ), - ("Alice -- Bob -- Daniel, Cecil:Gordon, Helen", + ( + "Alice -- Bob -- Daniel, Cecil:Gordon, Helen", ["Alice", "Bob", "Daniel", "Cecil", "Gordon", "Helen"], - [(0,1),(1,2)] + [(0, 1), (1, 2)], ), - ('"+" -- "-", "*" -- "/", "%%" -- "%/%"', + ( + '"+" -- "-", "*" -- "/", "%%" -- "%/%"', ["+", "-", "*", "/", "%%", "%/%"], - [(0,1),(2,3),(4,5)] + [(0, 1), (2, 3), (4, 5)], ), - ("A-B-C\nC-D", ["A", "B", "C", "D"], [(0,1),(1,2),(2,3)]), - ("A-B-C\n C-D", ["A", "B", "C", "D"], [(0,1),(1,2),(2,3)]) + ("A-B-C\nC-D", ["A", "B", "C", "D"], [(0, 1), (1, 2), (2, 3)]), + ("A-B-C\n C-D", ["A", "B", "C", "D"], [(0, 1), (1, 2), (2, 3)]), ] for formula, names, edges in tests: g = Graph.Formula(formula) @@ -84,20 +97,22 @@ def testFormula(self): self.assertEqual(g.get_edgelist(), sorted(edges)) def testFull(self): - g=Graph.Full(20, directed=True) - el=g.get_edgelist() + g = Graph.Full(20, directed=True) + el = g.get_edgelist() el.sort() - self.assertTrue(g.get_edgelist() == [(x, y) for x in range(20) for y in range(20) if x!=y]) + self.assertTrue( + g.get_edgelist() == [(x, y) for x in range(20) for y in range(20) if x != y] + ) def testFullCitation(self): - g=Graph.Full_Citation(20) - el=g.get_edgelist() + g = Graph.Full_Citation(20) + el = g.get_edgelist() el.sort() self.assertTrue(not g.is_directed()) - self.assertTrue(el == [(x, y) for x in range(19) for y in range(x+1, 20)]) + self.assertTrue(el == [(x, y) for x in range(19) for y in range(x + 1, 20)]) - g=Graph.Full_Citation(20, True) - el=g.get_edgelist() + g = Graph.Full_Citation(20, True) + el = g.get_edgelist() el.sort() self.assertTrue(g.is_directed()) self.assertTrue(el == [(x, y) for x in range(1, 20) for y in range(x)]) @@ -105,24 +120,24 @@ def testFullCitation(self): self.assertRaises(InternalError, Graph.Full_Citation, -2) def testLCF(self): - g1=Graph.LCF(12, (5, -5), 6) - g2=Graph.Famous("Franklin") + g1 = Graph.LCF(12, (5, -5), 6) + g2 = Graph.Famous("Franklin") self.assertTrue(g1.isomorphic(g2)) self.assertRaises(InternalError, Graph.LCF, 12, (5, -5), -3) def testKautz(self): - g=Graph.Kautz(2, 2) - deg_in=g.degree(mode=IN) - deg_out=g.degree(mode=OUT) + g = Graph.Kautz(2, 2) + deg_in = g.degree(mode=IN) + deg_out = g.degree(mode=OUT) # This is not a proper test, but should spot most errors - self.assertTrue(g.is_directed() and deg_in==[2]*12 and deg_out==[2]*12) + self.assertTrue(g.is_directed() and deg_in == [2] * 12 and deg_out == [2] * 12) def testDeBruijn(self): - g=Graph.De_Bruijn(2, 3) - deg_in=g.degree(mode=IN, loops=True) - deg_out=g.degree(mode=OUT, loops=True) + g = Graph.De_Bruijn(2, 3) + deg_in = g.degree(mode=IN, loops=True) + deg_out = g.degree(mode=OUT, loops=True) # This is not a proper test, but should spot most errors - self.assertTrue(g.is_directed() and deg_in==[2]*8 and deg_out==[2]*8) + self.assertTrue(g.is_directed() and deg_in == [2] * 8 and deg_out == [2] * 8) def testSBM(self): pref_matrix = [[0.5, 0, 0], [0, 0, 0.5], [0, 0.5, 0]] @@ -133,8 +148,8 @@ def testSBM(self): # Simple smoke tests for the expected structure of the graph self.assertTrue(g.is_simple()) self.assertFalse(g.is_directed()) - self.assertEqual([0]*20 + [1]*40, g.clusters().membership) - g2 = g.subgraph(range(20, 60)) + self.assertEqual([0] * 20 + [1] * 40, g.clusters().membership) + g2 = g.subgraph(list(range(20, 60))) self.assertTrue(not any(e.source // 20 == e.target // 20 for e in g2.es)) # Check loops argument @@ -159,65 +174,67 @@ def testWeightedAdjacency(self): g = Graph.Weighted_Adjacency(mat, attr="w0") el = g.get_edgelist() - self.assertTrue(el == [(0,1), (0,2), (1,0), (2,2), (3,1)]) + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() - self.assertTrue(el == [(0,1), (0,2), (1,3), (2,2)]) + self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() - self.assertTrue(el == [(0,1), (0,2), (1,0), (3,1)]) + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) @unittest.skipIf((np is None) or (pd is None), "test case depends on NumPy/Pandas") def testDataFrame(self): edges = pd.DataFrame( - [['C', 'A', 0.4], ['A', 'B', 0.1]], - columns=[0, 1, 'weight']) + [["C", "A", 0.4], ["A", "B", 0.1]], columns=[0, 1, "weight"] + ) g = Graph.DataFrame(edges, directed=False) self.assertTrue(g.es["weight"] == [0.4, 0.1]) vertices = pd.DataFrame( - [['A', 'blue'], ['B', 'yellow'], ['C', 'blue']], - columns=[0, 'color']) + [["A", "blue"], ["B", "yellow"], ["C", "blue"]], columns=[0, "color"] + ) g = Graph.DataFrame(edges, directed=True, vertices=vertices) - self.assertTrue(g.vs['name'] == ['A', 'B', 'C']) - self.assertTrue(g.vs["color"] == ['blue', 'yellow', 'blue']) + self.assertTrue(g.vs["name"] == ["A", "B", "C"]) + self.assertTrue(g.vs["color"] == ["blue", "yellow", "blue"]) self.assertTrue(g.es["weight"] == [0.4, 0.1]) # Issue #347 - edges = pd.DataFrame({'source': [1, 2, 3], 'target': [4, 5, 6]}) - vertices = pd.DataFrame({ - 'node': [1, 2, 3, 4, 5, 6], - 'label': ['1', '2', '3', '4', '5', '6']})[['node', 'label']] + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame( + {"node": [1, 2, 3, 4, 5, 6], "label": ["1", "2", "3", "4", "5", "6"]} + )[["node", "label"]] g = Graph.DataFrame( edges, directed=True, vertices=vertices, - ) - self.assertTrue(g.vs['name'] == [1, 2, 3, 4, 5, 6]) - self.assertTrue(g.vs['label'] == ['1', '2', '3', '4', '5', '6']) + ) + self.assertTrue(g.vs["name"] == [1, 2, 3, 4, 5, 6]) + self.assertTrue(g.vs["label"] == ["1", "2", "3", "4", "5", "6"]) # Vertex ids - edges = pd.DataFrame({'source': [1, 2, 3], 'target': [4, 5, 6]}) + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) g = Graph.DataFrame(edges) self.assertTrue(g.vcount() == 6) - edges = pd.DataFrame({'source': [1, 2, 3], 'target': [4, 5, 6]}) + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) g = Graph.DataFrame(edges, use_vids=True) self.assertTrue(g.vcount() == 7) + def suite(): generator_suite = unittest.makeSuite(GeneratorTests) return unittest.TestSuite([generator_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_homepage.py b/tests/test_homepage.py index a0e8dad1d..3dbb57129 100644 --- a/tests/test_homepage.py +++ b/tests/test_homepage.py @@ -2,6 +2,7 @@ from igraph import * + class HomepageExampleTests(unittest.TestCase): """Smoke tests for the Python examples found on the homepage to ensure that they do not break.""" @@ -11,7 +12,7 @@ def testErdosRenyiComponents(self): colors = ["lightgray", "cyan", "magenta", "yellow", "blue", "green", "red"] components = g.components() for component in components: - color = colors[min(6, len(components)-1)] + color = colors[min(6, len(components) - 1)] g.vs[component]["color"] = color # No plotting here, but we calculate the FR layout @@ -24,27 +25,27 @@ def testKautz(self): def testMSTofGRG(self): def distance(p1, p2): - return ((p1[0]-p2[0]) ** 2 + (p1[1]-p2[1]) ** 2) ** 0.5 + return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5 g = Graph.GRG(100, 0.2) - layout = Layout(zip(g.vs["x"], g.vs["y"])) + layout = Layout(list(zip(g.vs["x"], g.vs["y"]))) - weights = [distance(layout[edge.source], layout[edge.target]) \ - for edge in g.es] + weights = [distance(layout[edge.source], layout[edge.target]) for edge in g.es] max_weight = max(weights) - g.es["width"] = [6 - 5*weight / max_weight for weight in weights] + g.es["width"] = [6 - 5 * weight / max_weight for weight in weights] mst = g.spanning_tree(weights) # Plotting omitted + def suite(): homepage_example_suite = unittest.makeSuite(HomepageExampleTests) return unittest.TestSuite([homepage_example_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - - diff --git a/tests/test_indexing.py b/tests/test_indexing.py index 301afbc56..33fba0b7f 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -2,6 +2,7 @@ import unittest from igraph import * + class GraphAdjacencyMatrixLikeIndexingTests(unittest.TestCase): def testSingleEdgeRetrieval(self): g = Graph.Famous("krackhardt_kite") @@ -18,7 +19,7 @@ def testSingleEdgeRetrieval(self): def testSingleEdgeRetrievalWeights(self): g = Graph.Famous("krackhardt_kite") - g.es["weight"] = range(g.ecount()) + g.es["weight"] = list(range(g.ecount())) for idx, (v1, v2) in enumerate(g.get_edgelist()): self.assertEqual(g[v1, v2], idx) self.assertEqual(g[v2, v1], idx) @@ -29,10 +30,10 @@ def testSingleEdgeRetrievalWeights(self): def testSingleEdgeRetrievalAttrName(self): g = Graph.Famous("krackhardt_kite") - g.es["value"] = range(20, g.ecount()+20) + g.es["value"] = list(range(20, g.ecount() + 20)) for idx, (v1, v2) in enumerate(g.get_edgelist()): - self.assertEqual(g[v1, v2, "value"], idx+20) - self.assertEqual(g[v2, v1, "value"], idx+20) + self.assertEqual(g[v1, v2, "value"], idx + 20) + self.assertEqual(g[v2, v1, "value"], idx + 20) for v1 in range(g.vcount()): for v2 in set(range(g.vcount())) - set(g.neighbors(v1)): self.assertEqual(g[v1, v2, "value"], 0) @@ -43,9 +44,11 @@ def suite(): adjacency_suite = unittest.makeSuite(GraphAdjacencyMatrixLikeIndexingTests) return unittest.TestSuite([adjacency_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() diff --git a/tests/test_isomorphism.py b/tests/test_isomorphism.py index b8e6c0256..d5042ec83 100644 --- a/tests/test_isomorphism.py +++ b/tests/test_isomorphism.py @@ -3,138 +3,206 @@ from itertools import permutations from random import shuffle + def node_compat(g1, g2, v1, v2): """Node compatibility function for isomorphism tests""" return g1.vs[v1]["color"] == g2.vs[v2]["color"] + def edge_compat(g1, g2, e1, e2): """Edge compatibility function for isomorphism tests""" return g1.es[e1]["color"] == g2.es[e2]["color"] + class IsomorphismTests(unittest.TestCase): def testIsomorphic(self): - g1 = Graph(8, [(0, 4), (0, 5), (0, 6), \ - (1, 4), (1, 5), (1, 7), \ - (2, 4), (2, 6), (2, 7), \ - (3, 5), (3, 6), (3, 7)]) - g2 = Graph(8, [(0, 1), (0, 3), (0, 4), \ - (2, 3), (2, 1), (2, 6), \ - (5, 1), (5, 4), (5, 6), \ - (7, 3), (7, 6), (7, 4)]) + g1 = Graph( + 8, + [ + (0, 4), + (0, 5), + (0, 6), + (1, 4), + (1, 5), + (1, 7), + (2, 4), + (2, 6), + (2, 7), + (3, 5), + (3, 6), + (3, 7), + ], + ) + g2 = Graph( + 8, + [ + (0, 1), + (0, 3), + (0, 4), + (2, 3), + (2, 1), + (2, 6), + (5, 1), + (5, 4), + (5, 6), + (7, 3), + (7, 6), + (7, 4), + ], + ) # Test the isomorphism of g1 and g2 self.assertTrue(g1.isomorphic(g2)) - self.assertTrue(g2.isomorphic_vf2(g1, return_mapping_21=True) \ - == (True, None, [0, 2, 5, 7, 1, 3, 4, 6])) - self.assertTrue(g2.isomorphic_bliss(g1, return_mapping_21=True, sh1="fl")\ - == (True, None, [0, 2, 5, 7, 1, 3, 4, 6])) + self.assertTrue( + g2.isomorphic_vf2(g1, return_mapping_21=True) + == (True, None, [0, 2, 5, 7, 1, 3, 4, 6]) + ) + self.assertTrue( + g2.isomorphic_bliss(g1, return_mapping_21=True, sh1="fl") + == (True, None, [0, 2, 5, 7, 1, 3, 4, 6]) + ) self.assertRaises(ValueError, g2.isomorphic_bliss, g1, sh2="nonexistent") # Test the automorphy of g1 self.assertTrue(g1.isomorphic()) - self.assertTrue(g1.isomorphic_vf2(return_mapping_21=True) \ - == (True, None, [0, 1, 2, 3, 4, 5, 6, 7])) + self.assertTrue( + g1.isomorphic_vf2(return_mapping_21=True) + == (True, None, [0, 1, 2, 3, 4, 5, 6, 7]) + ) # Test VF2 with colors - self.assertTrue(g1.isomorphic_vf2(g2, - color1=[0,1,0,1,0,1,0,1], - color2=[0,0,1,1,0,0,1,1])) - g1.vs["color"] = [0,1,0,1,0,1,0,1] - g2.vs["color"] = [0,0,1,1,0,1,1,0] + self.assertTrue( + g1.isomorphic_vf2( + g2, color1=[0, 1, 0, 1, 0, 1, 0, 1], color2=[0, 0, 1, 1, 0, 0, 1, 1] + ) + ) + g1.vs["color"] = [0, 1, 0, 1, 0, 1, 0, 1] + g2.vs["color"] = [0, 0, 1, 1, 0, 1, 1, 0] self.assertTrue(not g1.isomorphic_vf2(g2, "color", "color")) # Test bliss with colors - self.assertTrue(g1.isomorphic_bliss(g2, - color1=[0,0,0,0,0,0,0,0], - color2=[0,0,0,0,0,0,0,0])) - - self.assertTrue(g1.isomorphic_bliss(g2, - color1=[1,0,2,0,0,0,0,0], - color2=[1,0,2,0,0,0,0,0])) - - self.assertTrue(g1.isomorphic_bliss(g2, - color1=[0,1,0,1,0,1,0,1], - color2=[0,0,1,1,0,0,1,1])) + self.assertTrue( + g1.isomorphic_bliss( + g2, color1=[0, 0, 0, 0, 0, 0, 0, 0], color2=[0, 0, 0, 0, 0, 0, 0, 0] + ) + ) + + self.assertTrue( + g1.isomorphic_bliss( + g2, color1=[1, 0, 2, 0, 0, 0, 0, 0], color2=[1, 0, 2, 0, 0, 0, 0, 0] + ) + ) + + self.assertTrue( + g1.isomorphic_bliss( + g2, color1=[0, 1, 0, 1, 0, 1, 0, 1], color2=[0, 0, 1, 1, 0, 0, 1, 1] + ) + ) # Test VF2 with vertex and edge colors - self.assertTrue(g1.isomorphic_vf2(g2, - color1=[0,1,0,1,0,1,0,1], - color2=[0,0,1,1,0,0,1,1])) - g1.es["color"] = range(12) - g2.es["color"] = [0]*6 + [1]*6 + self.assertTrue( + g1.isomorphic_vf2( + g2, color1=[0, 1, 0, 1, 0, 1, 0, 1], color2=[0, 0, 1, 1, 0, 0, 1, 1] + ) + ) + g1.es["color"] = list(range(12)) + g2.es["color"] = [0] * 6 + [1] * 6 self.assertTrue(not g1.isomorphic_vf2(g2, "color", "color", "color", "color")) # Test VF2 with node compatibility function - g2.vs["color"] = [0,0,1,1,0,0,1,1] + g2.vs["color"] = [0, 0, 1, 1, 0, 0, 1, 1] self.assertTrue(g1.isomorphic_vf2(g2, node_compat_fn=node_compat)) - g2.vs["color"] = [0,0,1,1,0,1,1,0] + g2.vs["color"] = [0, 0, 1, 1, 0, 1, 1, 0] self.assertTrue(not g1.isomorphic_vf2(g2, node_compat_fn=node_compat)) # Test VF2 with node edge compatibility function - g2.vs["color"] = [0,0,1,1,0,0,1,1] - g1.es["color"] = range(12) - g2.es["color"] = [0]*6 + [1]*6 - self.assertTrue(not g1.isomorphic_vf2(g2, node_compat_fn=node_compat, - edge_compat_fn=edge_compat)) + g2.vs["color"] = [0, 0, 1, 1, 0, 0, 1, 1] + g1.es["color"] = list(range(12)) + g2.es["color"] = [0] * 6 + [1] * 6 + self.assertTrue( + not g1.isomorphic_vf2( + g2, node_compat_fn=node_compat, edge_compat_fn=edge_compat + ) + ) def testIsomorphicCallback(self): maps = [] + def callback(g1, g2, map1, map2): maps.append(map1) return True # Test VF2 callback - g = Graph(6, [(0,1), (2,3), (4,5), (0,2), (2,4), (1,3), (3,5)]) + g = Graph(6, [(0, 1), (2, 3), (4, 5), (0, 2), (2, 4), (1, 3), (3, 5)]) g.isomorphic_vf2(g, callback=callback) - expected_maps = [[0,1,2,3,4,5], [1,0,3,2,5,4], [4,5,2,3,0,1], [5,4,3,2,1,0]] + expected_maps = [ + [0, 1, 2, 3, 4, 5], + [1, 0, 3, 2, 5, 4], + [4, 5, 2, 3, 0, 1], + [5, 4, 3, 2, 1, 0], + ] self.assertTrue(sorted(maps) == expected_maps) maps[:] = [] g3 = Graph.Full(4) - g3.vs["color"] = [0,1,1,0] + g3.vs["color"] = [0, 1, 1, 0] g3.isomorphic_vf2(callback=callback, color1="color", color2="color") - expected_maps = [[0,1,2,3], [0,2,1,3], [3,1,2,0], [3,2,1,0]] + expected_maps = [[0, 1, 2, 3], [0, 2, 1, 3], [3, 1, 2, 0], [3, 2, 1, 0]] self.assertTrue(sorted(maps) == expected_maps) def testCountIsomorphisms(self): g = Graph.Full(4) self.assertTrue(g.count_automorphisms_vf2() == 24) - g = Graph(6, [(0,1), (2,3), (4,5), (0,2), (2,4), (1,3), (3,5)]) + g = Graph(6, [(0, 1), (2, 3), (4, 5), (0, 2), (2, 4), (1, 3), (3, 5)]) self.assertTrue(g.count_automorphisms_vf2() == 4) # Some more tests with colors g3 = Graph.Full(4) - g3.vs["color"] = [0,1,1,0] + g3.vs["color"] = [0, 1, 1, 0] self.assertTrue(g3.count_isomorphisms_vf2() == 24) self.assertTrue(g3.count_isomorphisms_vf2(color1="color", color2="color") == 4) - self.assertTrue(g3.count_isomorphisms_vf2(color1=[0,1,2,0], color2=(0,1,2,0)) == 2) - self.assertTrue(g3.count_isomorphisms_vf2(edge_color1=[0,1,0,0,0,1], - edge_color2=[0,1,0,0,0,1]) == 2) + self.assertTrue( + g3.count_isomorphisms_vf2(color1=[0, 1, 2, 0], color2=(0, 1, 2, 0)) == 2 + ) + self.assertTrue( + g3.count_isomorphisms_vf2( + edge_color1=[0, 1, 0, 0, 0, 1], edge_color2=[0, 1, 0, 0, 0, 1] + ) + == 2 + ) # Test VF2 with node/edge compatibility function - g3.vs["color"] = [0,1,1,0] + g3.vs["color"] = [0, 1, 1, 0] self.assertTrue(g3.count_isomorphisms_vf2(node_compat_fn=node_compat) == 4) - g3.vs["color"] = [0,1,2,0] + g3.vs["color"] = [0, 1, 2, 0] self.assertTrue(g3.count_isomorphisms_vf2(node_compat_fn=node_compat) == 2) - g3.es["color"] = [0,1,0,0,0,1] + g3.es["color"] = [0, 1, 0, 0, 0, 1] self.assertTrue(g3.count_isomorphisms_vf2(edge_compat_fn=edge_compat) == 2) def testGetIsomorphisms(self): - g = Graph(6, [(0,1), (2,3), (4,5), (0,2), (2,4), (1,3), (3,5)]) + g = Graph(6, [(0, 1), (2, 3), (4, 5), (0, 2), (2, 4), (1, 3), (3, 5)]) maps = g.get_automorphisms_vf2() - expected_maps = [[0,1,2,3,4,5], [1,0,3,2,5,4], [4,5,2,3,0,1], [5,4,3,2,1,0]] + expected_maps = [ + [0, 1, 2, 3, 4, 5], + [1, 0, 3, 2, 5, 4], + [4, 5, 2, 3, 0, 1], + [5, 4, 3, 2, 1, 0], + ] self.assertTrue(maps == expected_maps) g3 = Graph.Full(4) - g3.vs["color"] = [0,1,1,0] - expected_maps = [[0,1,2,3], [0,2,1,3], [3,1,2,0], [3,2,1,0]] - self.assertTrue(sorted(g3.get_automorphisms_vf2(color="color")) == expected_maps) + g3.vs["color"] = [0, 1, 1, 0] + expected_maps = [[0, 1, 2, 3], [0, 2, 1, 3], [3, 1, 2, 0], [3, 2, 1, 0]] + self.assertTrue( + sorted(g3.get_automorphisms_vf2(color="color")) == expected_maps + ) + class SubisomorphismTests(unittest.TestCase): def testSubisomorphicLAD(self): - g = Graph.Lattice([3,3], circular=False) - g2 = Graph([(0,1), (1,2), (1,3)]) - g3 = g + [(0,4), (2,4), (6,4), (8,4), (3,1), (1,5), (5,7), (7,3)] + g = Graph.Lattice([3, 3], circular=False) + g2 = Graph([(0, 1), (1, 2), (1, 3)]) + g3 = g + [(0, 4), (2, 4), (6, 4), (8, 4), (3, 1), (1, 5), (5, 7), (7, 3)] self.assertTrue(g.subisomorphic_lad(g2)) self.assertFalse(g2.subisomorphic_lad(g)) @@ -148,9 +216,19 @@ def testSubisomorphicLAD(self): self.assertTrue(g3.subisomorphic_lad(g2)) # Test with limited vertex matching - domains = [[4], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8]] + domains = [ + [4], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + ] self.assertTrue(g.subisomorphic_lad(g2, domains=domains)) - domains = [[], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8]] + domains = [ + [], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + ] self.assertTrue(not g.subisomorphic_lad(g2, domains=domains)) # Corner cases @@ -159,9 +237,9 @@ def testSubisomorphicLAD(self): self.assertTrue(empty.subisomorphic_lad(empty)) def testGetSubisomorphismsLAD(self): - g = Graph.Lattice([3,3], circular=False) - g2 = Graph([(0,1), (1,2), (2,3), (3,0)]) - g3 = g + [(0,4), (2,4), (6,4), (8,4), (3,1), (1,5), (5,7), (7,3)] + g = Graph.Lattice([3, 3], circular=False) + g2 = Graph([(0, 1), (1, 2), (2, 3), (3, 0)]) + g3 = g + [(0, 4), (2, 4), (6, 4), (8, 4), (3, 1), (1, 5), (5, 7), (7, 3)] all_subiso = "0143 0341 1034 1254 1430 1452 2145 2541 3014 3410 3476 \ 3674 4103 4125 4301 4367 4521 4587 4763 4785 5214 5412 5478 5874 6347 \ @@ -173,18 +251,32 @@ def testGetSubisomorphismsLAD(self): # Test 'induced' induced_subiso = "1375 1573 3751 5731 7513 7315 5137 3157" - induced_subiso = sorted([int(x) for x in item] for item in induced_subiso.split()) + induced_subiso = sorted( + [int(x) for x in item] for item in induced_subiso.split() + ) all_subiso_extra = sorted(all_subiso + induced_subiso) - self.assertEqual(induced_subiso, - sorted(g3.get_subisomorphisms_lad(g2, induced=True))) + self.assertEqual( + induced_subiso, sorted(g3.get_subisomorphisms_lad(g2, induced=True)) + ) self.assertEqual([], g3.get_subisomorphisms_lad(g, induced=True)) # Test with limited vertex matching limited_subiso = [iso for iso in all_subiso if iso[0] == 4] - domains = [[4], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8]] - self.assertEqual(limited_subiso, - sorted(g.get_subisomorphisms_lad(g2, domains=domains))) - domains = [[], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8], [0,1,2,3,5,6,7,8]] + domains = [ + [4], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + ] + self.assertEqual( + limited_subiso, sorted(g.get_subisomorphisms_lad(g2, domains=domains)) + ) + domains = [ + [], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + [0, 1, 2, 3, 5, 6, 7, 8], + ] self.assertEqual([], sorted(g.get_subisomorphisms_lad(g2, domains=domains))) # Corner cases @@ -193,42 +285,50 @@ def testGetSubisomorphismsLAD(self): self.assertEqual([], empty.get_subisomorphisms_lad(empty)) def testSubisomorphicVF2(self): - g = Graph.Lattice([3,3], circular=False) - g2 = Graph([(0,1), (1,2), (1,3)]) + g = Graph.Lattice([3, 3], circular=False) + g2 = Graph([(0, 1), (1, 2), (1, 3)]) self.assertTrue(g.subisomorphic_vf2(g2)) self.assertTrue(not g2.subisomorphic_vf2(g)) # Test with vertex colors - g.vs["color"] = [0,0,0,0,1,0,0,0,0] - g2.vs["color"] = [1,0,0,0] + g.vs["color"] = [0, 0, 0, 0, 1, 0, 0, 0, 0] + g2.vs["color"] = [1, 0, 0, 0] self.assertTrue(g.subisomorphic_vf2(g2, node_compat_fn=node_compat)) - g2.vs["color"] = [2,0,0,0] + g2.vs["color"] = [2, 0, 0, 0] self.assertTrue(not g.subisomorphic_vf2(g2, node_compat_fn=node_compat)) # Test with edge colors - g.es["color"] = [1] + [0]*(g.ecount()-1) - g2.es["color"] = [1] + [0]*(g2.ecount()-1) + g.es["color"] = [1] + [0] * (g.ecount() - 1) + g2.es["color"] = [1] + [0] * (g2.ecount() - 1) self.assertTrue(g.subisomorphic_vf2(g2, edge_compat_fn=edge_compat)) g2.es[0]["color"] = [2] self.assertTrue(not g.subisomorphic_vf2(g2, node_compat_fn=node_compat)) def testCountSubisomorphisms(self): - g = Graph.Lattice([3,3], circular=False) - g2 = Graph.Lattice([2,2], circular=False) - self.assertTrue(g.count_subisomorphisms_vf2(g2) == 4*4*2) + g = Graph.Lattice([3, 3], circular=False) + g2 = Graph.Lattice([2, 2], circular=False) + self.assertTrue(g.count_subisomorphisms_vf2(g2) == 4 * 4 * 2) self.assertTrue(g2.count_subisomorphisms_vf2(g) == 0) # Test with vertex colors - g.vs["color"] = [0,0,0,0,1,0,0,0,0] - g2.vs["color"] = [1,0,0,0] - self.assertTrue(g.count_subisomorphisms_vf2(g2, "color", "color") == 4*2) - self.assertTrue(g.count_subisomorphisms_vf2(g2, node_compat_fn=node_compat) == 4*2) + g.vs["color"] = [0, 0, 0, 0, 1, 0, 0, 0, 0] + g2.vs["color"] = [1, 0, 0, 0] + self.assertTrue(g.count_subisomorphisms_vf2(g2, "color", "color") == 4 * 2) + self.assertTrue( + g.count_subisomorphisms_vf2(g2, node_compat_fn=node_compat) == 4 * 2 + ) # Test with edge colors - g.es["color"] = [1] + [0]*(g.ecount()-1) - g2.es["color"] = [1] + [0]*(g2.ecount()-1) - self.assertTrue(g.count_subisomorphisms_vf2(g2, edge_color1="color", edge_color2="color") == 2) - self.assertTrue(g.count_subisomorphisms_vf2(g2, edge_compat_fn=edge_compat) == 2) + g.es["color"] = [1] + [0] * (g.ecount() - 1) + g2.es["color"] = [1] + [0] * (g2.ecount() - 1) + self.assertTrue( + g.count_subisomorphisms_vf2(g2, edge_color1="color", edge_color2="color") + == 2 + ) + self.assertTrue( + g.count_subisomorphisms_vf2(g2, edge_compat_fn=edge_compat) == 2 + ) + class PermutationTests(unittest.TestCase): def testCanonicalPermutation(self): @@ -246,16 +346,15 @@ def testCanonicalPermutation(self): self.assertTrue(sorted(g3.get_edgelist()) == sorted(g4.get_edgelist())) # Simple case with coloring - cp = g1.canonical_permutation(color = [0, 0, 1, 1]) + cp = g1.canonical_permutation(color=[0, 0, 1, 1]) g3 = g1.permute_vertices(cp) - cp = g2.canonical_permutation(color = [0, 0, 1, 1]) + cp = g2.canonical_permutation(color=[0, 0, 1, 1]) g4 = g2.permute_vertices(cp) self.assertTrue(g3.vcount() == g4.vcount()) self.assertTrue(sorted(g3.get_edgelist()) == sorted(g4.get_edgelist())) - # More complicated one: small GRG, random permutation g = Graph.GRG(10, 0.5) perm = list(range(10)) @@ -268,30 +367,59 @@ def testCanonicalPermutation(self): self.assertTrue(sorted(g3.get_edgelist()) == sorted(g4.get_edgelist())) def testPermuteVertices(self): - g1 = Graph(8, [(0, 4), (0, 5), (0, 6), \ - (1, 4), (1, 5), (1, 7), \ - (2, 4), (2, 6), (2, 7), \ - (3, 5), (3, 6), (3, 7)]) - g2 = Graph(8, [(0, 1), (0, 3), (0, 4), \ - (2, 3), (2, 1), (2, 6), \ - (5, 1), (5, 4), (5, 6), \ - (7, 3), (7, 6), (7, 4)]) + g1 = Graph( + 8, + [ + (0, 4), + (0, 5), + (0, 6), + (1, 4), + (1, 5), + (1, 7), + (2, 4), + (2, 6), + (2, 7), + (3, 5), + (3, 6), + (3, 7), + ], + ) + g2 = Graph( + 8, + [ + (0, 1), + (0, 3), + (0, 4), + (2, 3), + (2, 1), + (2, 6), + (5, 1), + (5, 4), + (5, 6), + (7, 3), + (7, 6), + (7, 4), + ], + ) _, _, mapping = g1.isomorphic_vf2(g2, return_mapping_21=True) g3 = g2.permute_vertices(mapping) self.assertTrue(g3.vcount() == g2.vcount() and g3.ecount() == g2.ecount()) self.assertTrue(set(g3.get_edgelist()) == set(g1.get_edgelist())) + def suite(): isomorphism_suite = unittest.makeSuite(IsomorphismTests) subisomorphism_suite = unittest.makeSuite(SubisomorphismTests) permutation_suite = unittest.makeSuite(PermutationTests) - return unittest.TestSuite([isomorphism_suite, subisomorphism_suite, \ - permutation_suite]) + return unittest.TestSuite( + [isomorphism_suite, subisomorphism_suite, permutation_suite] + ) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_iterators.py b/tests/test_iterators.py index b8777d275..578c1cf6e 100644 --- a/tests/test_iterators.py +++ b/tests/test_iterators.py @@ -1,6 +1,7 @@ import unittest from igraph import * + class IteratorTests(unittest.TestCase): def testBFS(self): g = Graph.Tree(10, 2) @@ -16,9 +17,18 @@ def testBFSIter(self): vs = [(v, d, p.index) for v, d, p in vs if p is not None] self.assertEqual( vs, - [(1, 1, 0), (2, 1, 0), (3, 2, 1), - (4, 2, 1), (5, 2, 2), (6, 2, 2), - (7, 3, 3), (8, 3, 3), (9, 3, 4)]) + [ + (1, 1, 0), + (2, 1, 0), + (3, 2, 1), + (4, 2, 1), + (5, 2, 2), + (6, 2, 2), + (7, 3, 3), + (8, 3, 3), + (9, 3, 4), + ], + ) def testDFS(self): g = Graph.Tree(10, 2) @@ -34,18 +44,29 @@ def testDFSIter(self): vs = [(v, d, p.index) for v, d, p in vs if p is not None] self.assertEqual( vs, - [(1, 1, 0), (3, 2, 1), (7, 3, 3), - (8, 3, 3), (4, 2, 1), (9, 3, 4), - (2, 1, 0), (5, 2, 2), (6, 2, 2)]) + [ + (1, 1, 0), + (3, 2, 1), + (7, 3, 3), + (8, 3, 3), + (4, 2, 1), + (9, 3, 4), + (2, 1, 0), + (5, 2, 2), + (6, 2, 2), + ], + ) def suite(): iterator_suite = unittest.makeSuite(IteratorTests) return unittest.TestSuite([iterator_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() diff --git a/tests/test_layouts.py b/tests/test_layouts.py index af4c3181c..d4982622f 100644 --- a/tests/test_layouts.py +++ b/tests/test_layouts.py @@ -4,14 +4,14 @@ class LayoutTests(unittest.TestCase): def testConstructor(self): - layout = Layout([(0,0,1), (0,1,0), (1,0,0)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0)]) self.assertEqual(layout.dim, 3) - layout = Layout([(0,0,1), (0,1,0), (1,0,0)], 3) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0)], 3) self.assertEqual(layout.dim, 3) - self.assertRaises(ValueError, Layout, [(0,1), (1,0)], 3) + self.assertRaises(ValueError, Layout, [(0, 1), (1, 0)], 3) def testIndexing(self): - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) self.assertEqual(len(layout), 4) self.assertEqual(layout[1], [0, 1, 0]) self.assertEqual(layout[3], [2, 1, 3]) @@ -24,56 +24,47 @@ def testIndexing(self): self.assertEqual(len(layout), 3) def testScaling(self): - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) layout.scale(1.5) - self.assertEqual(layout.coords, [[0., 0., 1.5], \ - [0., 1.5, 0.], \ - [1.5, 0., 0.], \ - [3., 1.5, 4.5]]) + self.assertEqual( + layout.coords, + [[0.0, 0.0, 1.5], [0.0, 1.5, 0.0], [1.5, 0.0, 0.0], [3.0, 1.5, 4.5]], + ) - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) layout.scale(1, 1, 3) - self.assertEqual(layout.coords, [[0, 0, 3], \ - [0, 1, 0], \ - [1, 0, 0], \ - [2, 1, 9]]) + self.assertEqual(layout.coords, [[0, 0, 3], [0, 1, 0], [1, 0, 0], [2, 1, 9]]) - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) layout.scale((2, 2, 1)) - self.assertEqual(layout.coords, [[0, 0, 1], \ - [0, 2, 0], \ - [2, 0, 0], \ - [4, 2, 3]]) + self.assertEqual(layout.coords, [[0, 0, 1], [0, 2, 0], [2, 0, 0], [4, 2, 3]]) self.assertRaises(ValueError, layout.scale, 2, 3) def testTranslation(self): - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) layout2 = layout.copy() - layout.translate(1,3,2) - self.assertEqual(layout.coords, [[1, 3, 3], \ - [1, 4, 2], \ - [2, 3, 2], \ - [3, 4, 5]]) + layout.translate(1, 3, 2) + self.assertEqual(layout.coords, [[1, 3, 3], [1, 4, 2], [2, 3, 2], [3, 4, 5]]) - layout.translate((-1,-3,-2)) + layout.translate((-1, -3, -2)) self.assertEqual(layout.coords, layout2.coords) self.assertRaises(ValueError, layout.translate, v=[3]) def testCentroid(self): - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) centroid = layout.centroid() self.assertEqual(len(centroid), 3) self.assertAlmostEqual(centroid[0], 0.75) self.assertAlmostEqual(centroid[1], 0.5) - self.assertAlmostEqual(centroid[2], 1.) + self.assertAlmostEqual(centroid[2], 1.0) def testBoundaries(self): - layout = Layout([(0,0,1), (0,1,0), (1,0,0), (2,1,3)]) - self.assertEqual(layout.boundaries(), ([0,0,0],[2,1,3])) - self.assertEqual(layout.boundaries(1), ([-1,-1,-1],[3,2,4])) + layout = Layout([(0, 0, 1), (0, 1, 0), (1, 0, 0), (2, 1, 3)]) + self.assertEqual(layout.boundaries(), ([0, 0, 0], [2, 1, 3])) + self.assertEqual(layout.boundaries(1), ([-1, -1, -1], [3, 2, 4])) layout = Layout([]) self.assertRaises(ValueError, layout.boundaries) @@ -81,49 +72,51 @@ def testBoundaries(self): self.assertRaises(ValueError, layout.boundaries) def testBoundingBox(self): - layout = Layout([(0,1), (2,7)]) - self.assertEqual(layout.bounding_box(), BoundingBox(0,1,2,7)) - self.assertEqual(layout.bounding_box(1), BoundingBox(-1,0,3,8)) + layout = Layout([(0, 1), (2, 7)]) + self.assertEqual(layout.bounding_box(), BoundingBox(0, 1, 2, 7)) + self.assertEqual(layout.bounding_box(1), BoundingBox(-1, 0, 3, 8)) layout = Layout([]) - self.assertEqual(layout.bounding_box(), BoundingBox(0,0,0,0)) + self.assertEqual(layout.bounding_box(), BoundingBox(0, 0, 0, 0)) def testCenter(self): - layout = Layout([(-2,0), (-2,-2), (0,-2), (0,0)]) + layout = Layout([(-2, 0), (-2, -2), (0, -2), (0, 0)]) layout.center() - self.assertEqual(layout.coords, [[-1,1], [-1,-1], [1,-1], [1,1]]) - layout.center(5,5) - self.assertEqual(layout.coords, [[4,6], [4,4], [6,4], [6,6]]) + self.assertEqual(layout.coords, [[-1, 1], [-1, -1], [1, -1], [1, 1]]) + layout.center(5, 5) + self.assertEqual(layout.coords, [[4, 6], [4, 4], [6, 4], [6, 6]]) self.assertRaises(ValueError, layout.center, 3) self.assertRaises(TypeError, layout.center, p=6) def testFitInto(self): - layout = Layout([(-2,0), (-2,-2), (0,-2), (0,0)]) - layout.fit_into(BoundingBox(5,5,8,10), keep_aspect_ratio=False) + layout = Layout([(-2, 0), (-2, -2), (0, -2), (0, 0)]) + layout.fit_into(BoundingBox(5, 5, 8, 10), keep_aspect_ratio=False) self.assertEqual(layout.coords, [[5, 10], [5, 5], [8, 5], [8, 10]]) - layout = Layout([(-2,0), (-2,-2), (0,-2), (0,0)]) - layout.fit_into(BoundingBox(5,5,8,10)) + layout = Layout([(-2, 0), (-2, -2), (0, -2), (0, 0)]) + layout.fit_into(BoundingBox(5, 5, 8, 10)) self.assertEqual(layout.coords, [[5, 9], [5, 6], [8, 6], [8, 9]]) - layout = Layout([(-1,-1,-1), (0,0,0), (1,1,1), (2,2,0), (3,3,-1)]) - layout.fit_into((0,0,0,8,8,4)) - self.assertEqual(layout.coords, \ - [[0, 0, 0], [2, 2, 2], [4, 4, 4], [6, 6, 2], [8, 8, 0]] + layout = Layout([(-1, -1, -1), (0, 0, 0), (1, 1, 1), (2, 2, 0), (3, 3, -1)]) + layout.fit_into((0, 0, 0, 8, 8, 4)) + self.assertEqual( + layout.coords, [[0, 0, 0], [2, 2, 2], [4, 4, 4], [6, 6, 2], [8, 8, 0]] ) layout = Layout([]) - layout.fit_into((6,7,8,11)) + layout.fit_into((6, 7, 8, 11)) self.assertEqual(layout.coords, []) def testToPolar(self): layout = Layout([(0, 0), (-1, 1), (0, 1), (1, 1)]) layout.to_radial(min_angle=180, max_angle=0, max_radius=2) - exp = [[0., 0.], [-2., 0.], [0., 2.], [2, 0.]] + exp = [[0.0, 0.0], [-2.0, 0.0], [0.0, 2.0], [2, 0.0]] for idx in range(4): self.assertAlmostEqual(layout.coords[idx][0], exp[idx][0], places=3) self.assertAlmostEqual(layout.coords[idx][1], exp[idx][1], places=3) def testTransform(self): - def tr(coord, dx, dy): return coord[0]+dx, coord[1]+dy + def tr(coord, dx, dy): + return coord[0] + dx, coord[1] + dy + layout = Layout([(1, 2), (3, 4)]) layout.transform(tr, 2, -1) self.assertEqual(layout.coords, [[3, 1], [5, 3]]) @@ -155,30 +148,34 @@ def layout_test(graph, test_with_dims=(2, 3)): g["layout"] = "graphopt" layout_test(g, test_with_dims=()) - g.vs["x"] = range(20) - g.vs["y"] = range(20, 40) + g.vs["x"] = list(range(20)) + g.vs["y"] = list(range(20, 40)) layout_test(g, test_with_dims=()) del g["layout"] lo = layout_test(g, test_with_dims=(2,)) - self.assertEqual([tuple(item) for item in lo], - list(zip(range(20), range(20, 40)))) + self.assertEqual( + [tuple(item) for item in lo], + list(zip(list(range(20)), list(range(20, 40)))), + ) - g.vs["z"] = range(40, 60) + g.vs["z"] = list(range(40, 60)) lo = layout_test(g) - self.assertEqual([tuple(item) for item in lo], - list(zip(range(20), range(20, 40), range(40, 60)))) + self.assertEqual( + [tuple(item) for item in lo], + list(zip(list(range(20)), list(range(20, 40)), list(range(40, 60)))), + ) def testCircle(self): def test_is_proper_circular_layout(graph, layout): - xs, ys = zip(*layout) + xs, ys = list(zip(*layout)) n = graph.vcount() self.assertEqual(n, len(xs)) self.assertEqual(n, len(ys)) self.assertAlmostEqual(0, sum(xs)) self.assertAlmostEqual(0, sum(ys)) for x, y in zip(xs, ys): - self.assertAlmostEqual(1, x**2+y**2) + self.assertAlmostEqual(1, x ** 2 + y ** 2) g = Graph.Ring(8) layout = g.layout("circle") @@ -203,15 +200,17 @@ def testFruchtermanReingold(self): lo = g.layout("fr") self.assertTrue(isinstance(lo, Layout)) - lo = g.layout("fr", miny=range(100)) + lo = g.layout("fr", miny=list(range(100))) self.assertTrue(isinstance(lo, Layout)) self.assertTrue(all(lo[i][1] >= i for i in range(100))) - lo = g.layout("fr", miny=range(100), maxy=range(100)) + lo = g.layout("fr", miny=list(range(100)), maxy=list(range(100))) self.assertTrue(isinstance(lo, Layout)) self.assertTrue(all(lo[i][1] == i for i in range(100))) - lo = g.layout("fr", miny=[2]*100, maxy=[3]*100, minx=[4]*100, maxx=[6]*100) + lo = g.layout( + "fr", miny=[2] * 100, maxy=[3] * 100, minx=[4] * 100, maxx=[6] * 100 + ) self.assertTrue(isinstance(lo, Layout)) bbox = lo.bounding_box() self.assertTrue(bbox.top >= 2) @@ -222,13 +221,15 @@ def testFruchtermanReingold(self): def testFruchtermanReingoldGrid(self): g = Graph.Barabasi(100) for grid_opt in ["grid", "nogrid", "auto", True, False]: - lo = g.layout("fr", miny=range(100), grid=grid_opt) + lo = g.layout("fr", miny=list(range(100)), grid=grid_opt) self.assertTrue(isinstance(lo, Layout)) self.assertTrue(all(lo[i][1] >= i for i in range(100))) def testKamadaKawai(self): g = Graph.Barabasi(100) - lo = g.layout("kk", miny=[2]*100, maxy=[3]*100, minx=[4]*100, maxx=[6]*100) + lo = g.layout( + "kk", miny=[2] * 100, maxy=[3] * 100, minx=[4] * 100, maxx=[6] * 100 + ) self.assertTrue(isinstance(lo, Layout)) bbox = lo.bounding_box() self.assertTrue(bbox.top >= 2) @@ -257,9 +258,9 @@ def testReingoldTilford(self): self.assertEqual(ys, g.shortest_paths(root)[0]) g = Graph.Barabasi(100) + Graph.Barabasi(50) lo = g.layout("rt", root=[0, 100]) - self.assertEqual(lo[100][1]-lo[0][1], 0) + self.assertEqual(lo[100][1] - lo[0][1], 0) lo = g.layout("rt", root=[0, 100], rootlevel=[2, 10]) - self.assertEqual(lo[100][1]-lo[0][1], 8) + self.assertEqual(lo[100][1] - lo[0][1], 8) def testBipartite(self): g = Graph.Full_Bipartite(3, 2) @@ -273,8 +274,12 @@ def testBipartite(self): self.assertEqual([3, 3, 3, 0, 0], ys) lo = g.layout("bipartite", hgap=5) - self.assertEqual(set([0, 5, 10]), set(coord[0] for coord in lo if coord[1] == 1)) - self.assertEqual(set([2.5, 7.5]), set(coord[0] for coord in lo if coord[1] == 0)) + self.assertEqual( + set([0, 5, 10]), set(coord[0] for coord in lo if coord[1] == 1) + ) + self.assertEqual( + set([2.5, 7.5]), set(coord[0] for coord in lo if coord[1] == 0) + ) def testDRL(self): # Regression test for bug #1091891 diff --git a/tests/test_matching.py b/tests/test_matching.py index f84763e43..e7c91c989 100644 --- a/tests/test_matching.py +++ b/tests/test_matching.py @@ -2,6 +2,7 @@ from igraph import * + def powerset(iterable): items_powers = [(item, 1 << i) for i, item in enumerate(iterable)] for i in range(1 << len(items_powers)): @@ -10,17 +11,37 @@ def powerset(iterable): yield item -leda_graph = Graph([ - (0,8),(0,12),(0,14),(1,9),(1,10),(1,13), - (2,8),(2,9),(3,10),(3,11),(3,13),(4,9),(4,14), - (5,14),(6,9),(6,14),(7,8),(7,12),(7,14)]) -leda_graph.vs["type"] = [0]*8+[1]*7 +leda_graph = Graph( + [ + (0, 8), + (0, 12), + (0, 14), + (1, 9), + (1, 10), + (1, 13), + (2, 8), + (2, 9), + (3, 10), + (3, 11), + (3, 13), + (4, 9), + (4, 14), + (5, 14), + (6, 9), + (6, 14), + (7, 8), + (7, 12), + (7, 14), + ] +) +leda_graph.vs["type"] = [0] * 8 + [1] * 7 + class MatchingTests(unittest.TestCase): def setUp(self): - self.matching = Matching(leda_graph, - [12, 10, 8, 13, -1, 14, 9, -1, 2, 6, 1, -1, 0, 3, 5], - "type") + self.matching = Matching( + leda_graph, [12, 10, 8, 13, -1, 14, 9, -1, 2, 6, 1, -1, 0, 3, 5], "type" + ) def testIsMaximal(self): self.assertTrue(self.matching.is_maximal()) @@ -38,8 +59,10 @@ def testMatchingRetrieval(self): else: self.assertTrue(self.matching.is_matched(i)) self.assertEqual(self.matching.match_of(i), mate) - self.assertEqual(self.matching.match_of( - leda_graph.vs[i]).index, leda_graph.vs[mate].index) + self.assertEqual( + self.matching.match_of(leda_graph.vs[i]).index, + leda_graph.vs[mate].index, + ) class MaximumBipartiteMatchingTests(unittest.TestCase): @@ -57,12 +80,12 @@ def testBipartiteMatchingSimple(self): def testBipartiteMatchingErrors(self): # Type vector too short g = Graph([(0, 1), (1, 2), (2, 3)]) - self.assertRaises(InternalError, g.maximum_bipartite_matching, - types=[0,1,0]) + self.assertRaises(InternalError, g.maximum_bipartite_matching, types=[0, 1, 0]) # Graph not bipartite - self.assertRaises(InternalError, g.maximum_bipartite_matching, - types=[0,1,1,1]) + self.assertRaises( + InternalError, g.maximum_bipartite_matching, types=[0, 1, 1, 1] + ) def suite(): @@ -70,10 +93,11 @@ def suite(): bipartite_unweighted_suite = unittest.makeSuite(MaximumBipartiteMatchingTests) return unittest.TestSuite([matching_suite, bipartite_unweighted_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_operators.py b/tests/test_operators.py index 5261d4386..ec001dd46 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -9,6 +9,7 @@ except ImportError: np = None + class OperatorTests(unittest.TestCase): def testComplementer(self): g = Graph.Full(3) @@ -18,32 +19,53 @@ def testComplementer(self): g = Graph.Full(3) + Graph.Full(2) g2 = g.complementer(False) - self.assertTrue(sorted(g2.get_edgelist()) == [ - (0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4) - ]) + self.assertTrue( + sorted(g2.get_edgelist()) + == [(0, 3), (0, 4), (1, 3), (1, 4), (2, 3), (2, 4)] + ) g2 = g.complementer(loops=True) - self.assertTrue(sorted(g2.get_edgelist()) == [ - (0, 0), (0, 3), (0, 4), (1, 1), (1, 3), (1, 4), (2, 2), (2, 3), (2, 4), - (3, 3), (4, 4) - ]) + self.assertTrue( + sorted(g2.get_edgelist()) + == [ + (0, 0), + (0, 3), + (0, 4), + (1, 1), + (1, 3), + (1, 4), + (2, 2), + (2, 3), + (2, 4), + (3, 3), + (4, 4), + ] + ) def testMultiplication(self): g = Graph.Full(3) * 3 - self.assertTrue(g.vcount() == 9 and g.ecount() == 9 - and g.clusters().membership == [0, 0, 0, 1, 1, 1, 2, 2, 2]) + self.assertTrue( + g.vcount() == 9 + and g.ecount() == 9 + and g.clusters().membership == [0, 0, 0, 1, 1, 1, 2, 2, 2] + ) def testDifference(self): g = Graph.Tree(7, 2) - Graph.Lattice([7]) self.assertTrue(g.vcount() == 7 and g.ecount() == 5) - self.assertTrue(sorted(g.get_edgelist()) == [(0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]) + self.assertTrue( + sorted(g.get_edgelist()) == [(0, 2), (1, 3), (1, 4), (2, 5), (2, 6)] + ) def testDifferenceWithSelfLoop(self): # https://github.com/igraph/igraph/issues/597# g = Graph.Ring(10) + [(0, 0)] g -= Graph.Ring(5) self.assertTrue(g.vcount() == 10 and g.ecount() == 7) - self.assertTrue(sorted(g.get_edgelist()) == [(0, 0), (0, 9), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)]) + self.assertTrue( + sorted(g.get_edgelist()) + == [(0, 0), (0, 9), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9)] + ) def testIntersection(self): g = Graph.Tree(7, 2) & Graph.Lattice([7]) @@ -90,10 +112,23 @@ def testDisjointUnionSingle(self): def testUnion(self): g = Graph.Tree(7, 2) | Graph.Lattice([7]) self.assertTrue(g.vcount() == 7 and g.ecount() == 12) - self.assertTrue(sorted(g.get_edgelist()) == [ - (0, 1), (0, 2), (0, 6), (1, 2), (1, 3), (1, 4), (2, 3), (2, 5), - (2, 6), (3, 4), (4, 5), (5, 6) - ]) + self.assertTrue( + sorted(g.get_edgelist()) + == [ + (0, 1), + (0, 2), + (0, 6), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 5), + (2, 6), + (3, 4), + (4, 5), + (5, 6), + ] + ) def testUnionMethod(self): g = Graph.Tree(7, 2).union(Graph.Lattice([7])) @@ -117,36 +152,38 @@ def testUnionMany(self): def testUnionManyAttributes(self): gs = [ - Graph.Formula('A-B'), - Graph.Formula('A-B,C-D'), - ] - gs[0]['attr'] = 'graph1' - gs[0].vs['attr'] = ['set', 'set_too'] - gs[0].vs['attr2'] = ['set', 'set_too'] - gs[1].vs[0]['attr'] = 'set' - gs[1].vs[0]['attr2'] = 'conflict' + Graph.Formula("A-B"), + Graph.Formula("A-B,C-D"), + ] + gs[0]["attr"] = "graph1" + gs[0].vs["attr"] = ["set", "set_too"] + gs[0].vs["attr2"] = ["set", "set_too"] + gs[1].vs[0]["attr"] = "set" + gs[1].vs[0]["attr2"] = "conflict" g = union(gs) - names = g.vs['name'] - self.assertTrue(g['attr'] == 'graph1') - self.assertTrue(g.vs[names.index('A')]['attr'] == 'set') - self.assertTrue(g.vs[names.index('B')]['attr'] == 'set_too') + names = g.vs["name"] + self.assertTrue(g["attr"] == "graph1") + self.assertTrue(g.vs[names.index("A")]["attr"] == "set") + self.assertTrue(g.vs[names.index("B")]["attr"] == "set_too") self.assertTrue(g.ecount() == 2) - self.assertTrue(sorted(g.vertex_attributes()) == ['attr', 'attr2_1', 'attr2_2', 'name']) + self.assertTrue( + sorted(g.vertex_attributes()) == ["attr", "attr2_1", "attr2_2", "name"] + ) def testUnionManyEdgemap(self): gs = [ - Graph.Formula('A-B'), - Graph.Formula('C-D, A-B'), - ] - gs[0].es[0]['attr'] = 'set' - gs[1].es[0]['attr'] = 'set_too' + Graph.Formula("A-B"), + Graph.Formula("C-D, A-B"), + ] + gs[0].es[0]["attr"] = "set" + gs[1].es[0]["attr"] = "set_too" g = union(gs) for e in g.es: - vnames = [g.vs[e.source]['name'], g.vs[e.target]['name']] - if set(vnames) == set(['A', 'B']): - self.assertTrue(e['attr'] == 'set') + vnames = [g.vs[e.source]["name"], g.vs[e.target]["name"]] + if set(vnames) == set(["A", "B"]): + self.assertTrue(e["attr"] == "set") else: - self.assertTrue(e['attr'] == 'set_too') + self.assertTrue(e["attr"] == "set_too") def testIntersectionNoGraphs(self): self.assertRaises(ValueError, intersection, []) @@ -166,30 +203,30 @@ def testIntersectionMany(self): def testIntersectionManyAttributes(self): gs = [Graph.Tree(7, 2), Graph.Lattice([7])] - gs[0]['attr'] = 'graph1' - gs[0].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] - gs[1].vs['name'] = ['one', 'two', 'three', 'four', 'five', 'six', '7'] - gs[0].vs[0]['attr'] = 'set' - gs[1].vs[5]['attr'] = 'set_too' + gs[0]["attr"] = "graph1" + gs[0].vs["name"] = ["one", "two", "three", "four", "five", "six", "7"] + gs[1].vs["name"] = ["one", "two", "three", "four", "five", "six", "7"] + gs[0].vs[0]["attr"] = "set" + gs[1].vs[5]["attr"] = "set_too" g = intersection(gs) - names = g.vs['name'] - self.assertTrue(g['attr'] == 'graph1') - self.assertTrue(g.vs[names.index('one')]['attr'] == 'set') - self.assertTrue(g.vs[names.index('six')]['attr'] == 'set_too') + names = g.vs["name"] + self.assertTrue(g["attr"] == "graph1") + self.assertTrue(g.vs[names.index("one")]["attr"] == "set") + self.assertTrue(g.vs[names.index("six")]["attr"] == "set_too") self.assertTrue(g.ecount() == 1) self.assertTrue( - set(g.get_edgelist()[0]) == set([names.index('one'), names.index('two')]), + set(g.get_edgelist()[0]) == set([names.index("one"), names.index("two")]), ) def testIntersectionManyEdgemap(self): gs = [ - Graph.Formula('A-B'), - Graph.Formula('A-B,C-D'), - ] - gs[0].es[0]['attr'] = 'set' - gs[1].es[1]['attr'] = 'set_too' + Graph.Formula("A-B"), + Graph.Formula("A-B,C-D"), + ] + gs[0].es[0]["attr"] = "set" + gs[1].es[1]["attr"] = "set_too" g = intersection(gs) - self.assertTrue(g.es['attr'] == ['set']) + self.assertTrue(g.es["attr"] == ["set"]) def testInPlaceAddition(self): g = Graph.Full(3) @@ -197,28 +234,39 @@ def testInPlaceAddition(self): # Adding vertices g += 2 - self.assertTrue(g.vcount() == 5 and g.ecount() == 3 - and g.clusters().membership == [0,0,0,1,2]) + self.assertTrue( + g.vcount() == 5 + and g.ecount() == 3 + and g.clusters().membership == [0, 0, 0, 1, 2] + ) # Adding a vertex by name g += "spam" - self.assertTrue(g.vcount() == 6 and g.ecount() == 3 - and g.clusters().membership == [0,0,0,1,2,3]) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 3 + and g.clusters().membership == [0, 0, 0, 1, 2, 3] + ) # Adding a single edge g += (2, 3) - self.assertTrue(g.vcount() == 6 and g.ecount() == 4 - and g.clusters().membership == [0,0,0,0,1,2]) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 4 + and g.clusters().membership == [0, 0, 0, 0, 1, 2] + ) # Adding two edges g += [(3, 4), (2, 4), (4, 5)] - self.assertTrue(g.vcount() == 6 and g.ecount() == 7 - and g.clusters().membership == [0]*6) + self.assertTrue( + g.vcount() == 6 and g.ecount() == 7 and g.clusters().membership == [0] * 6 + ) # Adding two more vertices g += ["eggs", "bacon"] - self.assertEqual(g.vs["name"], [None, None, None, None, None, - "spam", "eggs", "bacon"]) + self.assertEqual( + g.vs["name"], [None, None, None, None, None, "spam", "eggs", "bacon"] + ) # Did we really use the original graph so far? # TODO: disjoint union should be modified so that this assertion @@ -227,45 +275,65 @@ def testInPlaceAddition(self): # Adding another graph g += Graph.Full(3) - self.assertTrue(g.vcount() == 11 and g.ecount() == 10 - and g.clusters().membership == [0,0,0,0,0,0,1,2,3,3,3]) + self.assertTrue( + g.vcount() == 11 + and g.ecount() == 10 + and g.clusters().membership == [0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3] + ) # Adding two graphs g += [Graph.Full(3), Graph.Full(2)] - self.assertTrue(g.vcount() == 16 and g.ecount() == 14 - and g.clusters().membership == [0,0,0,0,0,0,1,2,3,3,3,4,4,4,5,5]) + self.assertTrue( + g.vcount() == 16 + and g.ecount() == 14 + and g.clusters().membership + == [0, 0, 0, 0, 0, 0, 1, 2, 3, 3, 3, 4, 4, 4, 5, 5] + ) def testAddition(self): g0 = Graph.Full(3) # Adding vertices - g = g0+2 - self.assertTrue(g.vcount() == 5 and g.ecount() == 3 - and g.clusters().membership == [0,0,0,1,2]) + g = g0 + 2 + self.assertTrue( + g.vcount() == 5 + and g.ecount() == 3 + and g.clusters().membership == [0, 0, 0, 1, 2] + ) g0 = g # Adding vertices by name - g = g0+"spam" - self.assertTrue(g.vcount() == 6 and g.ecount() == 3 - and g.clusters().membership == [0,0,0,1,2,3]) + g = g0 + "spam" + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 3 + and g.clusters().membership == [0, 0, 0, 1, 2, 3] + ) g0 = g # Adding a single edge - g = g0+(2,3) - self.assertTrue(g.vcount() == 6 and g.ecount() == 4 - and g.clusters().membership == [0,0,0,0,1,2]) + g = g0 + (2, 3) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 4 + and g.clusters().membership == [0, 0, 0, 0, 1, 2] + ) g0 = g # Adding two edges - g = g0+[(3, 4), (2, 4), (4, 5)] - self.assertTrue(g.vcount() == 6 and g.ecount() == 7 - and g.clusters().membership == [0]*6) + g = g0 + [(3, 4), (2, 4), (4, 5)] + self.assertTrue( + g.vcount() == 6 and g.ecount() == 7 and g.clusters().membership == [0] * 6 + ) g0 = g # Adding another graph - g = g0+Graph.Full(3) - self.assertTrue(g.vcount() == 9 and g.ecount() == 10 - and g.clusters().membership == [0,0,0,0,0,0,1,1,1]) + g = g0 + Graph.Full(3) + self.assertTrue( + g.vcount() == 9 + and g.ecount() == 10 + and g.clusters().membership == [0, 0, 0, 0, 0, 0, 1, 1, 1] + ) def testInPlaceSubtraction(self): g = Graph.Full(8) @@ -273,28 +341,43 @@ def testInPlaceSubtraction(self): # Deleting a vertex by vertex selector g -= 7 - self.assertTrue(g.vcount() == 7 and g.ecount() == 21 - and g.clusters().membership == [0,0,0,0,0,0,0]) + self.assertTrue( + g.vcount() == 7 + and g.ecount() == 21 + and g.clusters().membership == [0, 0, 0, 0, 0, 0, 0] + ) # Deleting a vertex g -= g.vs[6] - self.assertTrue(g.vcount() == 6 and g.ecount() == 15 - and g.clusters().membership == [0,0,0,0,0,0]) + self.assertTrue( + g.vcount() == 6 + and g.ecount() == 15 + and g.clusters().membership == [0, 0, 0, 0, 0, 0] + ) # Deleting two vertices g -= [4, 5] - self.assertTrue(g.vcount() == 4 and g.ecount() == 6 - and g.clusters().membership == [0,0,0,0]) + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 6 + and g.clusters().membership == [0, 0, 0, 0] + ) # Deleting an edge g -= (1, 2) - self.assertTrue(g.vcount() == 4 and g.ecount() == 5 - and g.clusters().membership == [0,0,0,0]) + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 5 + and g.clusters().membership == [0, 0, 0, 0] + ) # Deleting three more edges g -= [(1, 3), (0, 2), (0, 3)] - self.assertTrue(g.vcount() == 4 and g.ecount() == 2 - and g.clusters().membership == [0,0,1,1]) + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 2 + and g.clusters().membership == [0, 0, 1, 1] + ) # Did we really use the original graph so far? self.assertTrue(id(g) == id(orig)) @@ -302,8 +385,11 @@ def testInPlaceSubtraction(self): # Subtracting a graph g2 = Graph.Tree(3, 2) g -= g2 - self.assertTrue(g.vcount() == 4 and g.ecount() == 1 - and g.clusters().membership == [0,1,2,2]) + self.assertTrue( + g.vcount() == 4 + and g.ecount() == 1 + and g.clusters().membership == [0, 1, 2, 2] + ) def testNonzero(self): self.assertTrue(Graph(1)) @@ -315,7 +401,7 @@ def testLength(self): self.assertTrue(len(Graph.Full(5).es) == 10) def testSimplify(self): - el = [(0,1), (1,0), (1,2), (2,3), (2,3), (2,3), (3,3)] + el = [(0, 1), (1, 0), (1, 2), (2, 3), (2, 3), (2, 3), (3, 3)] g = Graph(el) g.es["weight"] = [1, 2, 3, 4, 5, 6, 7] @@ -341,17 +427,49 @@ def testContractVertices(self): g2.contract_vertices([0, 1, 2, 3, 1, 0, 4, 5]) self.assertEqual(g2.vcount(), 6) self.assertEqual(g2.ecount(), g.ecount()) - self.assertEqual(sorted(g2.get_edgelist()), - [(0, 0), (0, 1), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), - (1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (4, 5)]) + self.assertEqual( + sorted(g2.get_edgelist()), + [ + (0, 0), + (0, 1), + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5), + (2, 3), + (4, 5), + ], + ) g2 = g.copy() g2.contract_vertices([0, 1, 2, 3, 1, 0, 6, 7]) self.assertEqual(g2.vcount(), 8) self.assertEqual(g2.ecount(), g.ecount()) - self.assertEqual(sorted(g2.get_edgelist()), - [(0, 0), (0, 1), (0, 1), (0, 2), (0, 3), (0, 6), (0, 7), - (1, 1), (1, 2), (1, 3), (1, 6), (1, 7), (2, 3), (6, 7)]) + self.assertEqual( + sorted(g2.get_edgelist()), + [ + (0, 0), + (0, 1), + (0, 1), + (0, 2), + (0, 3), + (0, 6), + (0, 7), + (1, 1), + (1, 2), + (1, 3), + (1, 6), + (1, 7), + (2, 3), + (6, 7), + ], + ) g2 = Graph(10) g2.contract_vertices([0, 0, 1, 1, 2, 2, 3, 3, 4, 4]) @@ -365,19 +483,36 @@ def testContractVerticesWithNumPyIntegers(self): g2.contract_vertices([np.int32(x) for x in [0, 1, 2, 3, 1, 0, 6, 7]]) self.assertEqual(g2.vcount(), 8) self.assertEqual(g2.ecount(), g.ecount()) - self.assertEqual(sorted(g2.get_edgelist()), - [(0, 0), (0, 1), (0, 1), (0, 2), (0, 3), (0, 6), (0, 7), - (1, 1), (1, 2), (1, 3), (1, 6), (1, 7), (2, 3), (6, 7)]) + self.assertEqual( + sorted(g2.get_edgelist()), + [ + (0, 0), + (0, 1), + (0, 1), + (0, 2), + (0, 3), + (0, 6), + (0, 7), + (1, 1), + (1, 2), + (1, 3), + (1, 6), + (1, 7), + (2, 3), + (6, 7), + ], + ) def suite(): operator_suite = unittest.makeSuite(OperatorTests) return unittest.TestSuite([operator_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_rng.py b/tests/test_rng.py index 29ca51492..00f82d8c4 100644 --- a/tests/test_rng.py +++ b/tests/test_rng.py @@ -16,6 +16,7 @@ def randint(a, b): def gauss(mu, sigma): return 0.3 + class InvalidRNG(object): pass @@ -30,8 +31,7 @@ def testSetRandomNumberGenerator(self): self.assertEqual(graph.vs["x"], [0.1] * 10) self.assertEqual(graph.vs["y"], [0.1] * 10) - self.assertRaises(AttributeError, set_random_number_generator, - InvalidRNG) + self.assertRaises(AttributeError, set_random_number_generator, InvalidRNG) def testSeeding(self): state = random.getstate() @@ -45,10 +45,11 @@ def suite(): random_suite = unittest.makeSuite(RandomNumberGeneratorTests) return unittest.TestSuite([random_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_separators.py b/tests/test_separators.py index caabc54c0..187026f63 100644 --- a/tests/test_separators.py +++ b/tests/test_separators.py @@ -2,6 +2,7 @@ from igraph import * + def powerset(iterable): items_powers = [(item, 1 << i) for i, item in enumerate(iterable)] for i in range(1 << len(items_powers)): @@ -9,6 +10,7 @@ def powerset(iterable): if i & power: yield item + class IsSeparatorTests(unittest.TestCase): def testIsSeparator(self): g = Graph.Lattice([8, 4], circular=False) @@ -20,9 +22,9 @@ def testIsSeparator(self): g = Graph.Lattice([8, 4], circular=True) self.assertFalse(g.is_separator([3, 11, 19, 27])) self.assertFalse(g.is_separator([29, 20, 11, 2])) - self.assertFalse(g.is_separator(range(32))) + self.assertFalse(g.is_separator(list(range(32)))) - self.assertRaises(InternalError, g.is_separator, range(33)) + self.assertRaises(InternalError, g.is_separator, list(range(33))) def testIsMinimalSeparator(self): g = Graph.Lattice([8, 4], circular=False) @@ -30,14 +32,14 @@ def testIsMinimalSeparator(self): self.assertFalse(g.is_minimal_separator([3, 11, 19, 27, 28])) self.assertFalse(g.is_minimal_separator([16, 25, 17])) self.assertTrue(g.is_minimal_separator([16, 25])) - self.assertFalse(g.is_minimal_separator(range(32))) + self.assertFalse(g.is_minimal_separator(list(range(32)))) - self.assertRaises(InternalError, g.is_minimal_separator, range(33)) + self.assertRaises(InternalError, g.is_minimal_separator, list(range(33))) def testAllMinimalSTSeparators(self): g = Graph.Famous("petersen") min_st_seps = set(tuple(x) for x in g.all_minimal_st_separators()) - for vs in powerset(range(g.vcount())): + for vs in powerset(list(range(g.vcount()))): if vs in min_st_seps: self.assertTrue(g.is_minimal_separator(vs)) else: @@ -52,18 +54,20 @@ def testMinimumSizeSeparators(self): size = len(min_size_seps[0]) self.assertTrue(len(s) != size for s in min_size_seps) - self.assertTrue(sum(1 for s in min_st_seps if len(s) == size) == - len(min_size_seps)) + self.assertTrue( + sum(1 for s in min_st_seps if len(s) == size) == len(min_size_seps) + ) def suite(): is_separator_suite = unittest.makeSuite(IsSeparatorTests) return unittest.TestSuite([is_separator_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_spectral.py b/tests/test_spectral.py index f2d75f441..d0912fb13 100644 --- a/tests/test_spectral.py +++ b/tests/test_spectral.py @@ -2,44 +2,53 @@ import unittest from igraph import * + class SpectralTests(unittest.TestCase): - def assertAlmostEqualMatrix(self, mat1, mat2, eps = 1e-7): - self.assertTrue(all( - abs(obs-exp) < eps - for obs, exp in zip(sum(mat1, []), sum(mat2, [])) - )) + def assertAlmostEqualMatrix(self, mat1, mat2, eps=1e-7): + self.assertTrue( + all(abs(obs - exp) < eps for obs, exp in zip(sum(mat1, []), sum(mat2, []))) + ) def testLaplacian(self): - g=Graph.Full(3) + g = Graph.Full(3) g.es["weight"] = [1, 2, 3] - self.assertTrue(g.laplacian() == [[ 2, -1, -1],\ - [-1, 2, -1],\ - [-1, -1, 2]]) - self.assertAlmostEqualMatrix(g.laplacian(normalized=True), - [[1, -0.5, -0.5], [-0.5, 1, -0.5], [-0.5, -0.5, 1]]) - - mx0 = [[1., -1/(12**0.5), -2/(15**0.5)], - [-1/(12**0.5), 1., -3/(20**0.5)], - [-2/(15**0.5), -3/(20**0.5), 1.]] + self.assertTrue(g.laplacian() == [[2, -1, -1], [-1, 2, -1], [-1, -1, 2]]) + self.assertAlmostEqualMatrix( + g.laplacian(normalized=True), + [[1, -0.5, -0.5], [-0.5, 1, -0.5], [-0.5, -0.5, 1]], + ) + + mx0 = [ + [1.0, -1 / (12 ** 0.5), -2 / (15 ** 0.5)], + [-1 / (12 ** 0.5), 1.0, -3 / (20 ** 0.5)], + [-2 / (15 ** 0.5), -3 / (20 ** 0.5), 1.0], + ] self.assertAlmostEqualMatrix(g.laplacian("weight", True), mx0) - g=Graph.Tree(5, 2) + g = Graph.Tree(5, 2) g.add_vertices(1) - self.assertTrue(g.laplacian() == [[ 2, -1, -1, 0, 0, 0],\ - [-1, 3, 0, -1, -1, 0],\ - [-1, 0, 1, 0, 0, 0],\ - [ 0, -1, 0, 1, 0, 0],\ - [ 0, -1, 0, 0, 1, 0],\ - [ 0, 0, 0, 0, 0, 0]]) - + self.assertTrue( + g.laplacian() + == [ + [2, -1, -1, 0, 0, 0], + [-1, 3, 0, -1, -1, 0], + [-1, 0, 1, 0, 0, 0], + [0, -1, 0, 1, 0, 0], + [0, -1, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0], + ] + ) + + def suite(): spectral_suite = unittest.makeSuite(SpectralTests) return unittest.TestSuite([spectral_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_structural.py b/tests/test_structural.py index e3e72d516..aadf18033 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -1,5 +1,3 @@ -from __future__ import division - import math import unittest import warnings @@ -7,27 +5,29 @@ from igraph import * from math import isnan + class SimplePropertiesTests(unittest.TestCase): - gfull = Graph.Full(10) + gfull = Graph.Full(10) gempty = Graph(10) g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) - gdir = Graph(4, [(0, 1), (0, 2), (1, 2), (2, 1), (0, 3), (1, 3), (3, 0)], directed=True) + gdir = Graph( + 4, [(0, 1), (0, 2), (1, 2), (2, 1), (0, 3), (1, 3), (3, 0)], directed=True + ) tree = Graph.Tree(14, 3) def testDensity(self): self.assertAlmostEqual(1.0, self.gfull.density(), places=5) self.assertAlmostEqual(0.0, self.gempty.density(), places=5) - self.assertAlmostEqual(5/6, self.g.density(), places=5) - self.assertAlmostEqual(1/2, self.g.density(True), places=5) - self.assertAlmostEqual(7/12, self.gdir.density(), places=5) - self.assertAlmostEqual(7/16, self.gdir.density(True), places=5) - self.assertAlmostEqual(1/7, self.tree.density(), places=5) + self.assertAlmostEqual(5 / 6, self.g.density(), places=5) + self.assertAlmostEqual(1 / 2, self.g.density(True), places=5) + self.assertAlmostEqual(7 / 12, self.gdir.density(), places=5) + self.assertAlmostEqual(7 / 16, self.gdir.density(True), places=5) + self.assertAlmostEqual(1 / 7, self.tree.density(), places=5) def testDiameter(self): self.assertTrue(self.gfull.diameter() == 1) self.assertTrue(self.gempty.diameter(unconn=False) == 10) - self.assertTrue(self.gempty.diameter(unconn=False, weights=[]) \ - == float('inf')) + self.assertTrue(self.gempty.diameter(unconn=False, weights=[]) == float("inf")) self.assertTrue(self.g.diameter() == 2) self.assertTrue(self.gdir.diameter(False) == 2) self.assertTrue(self.gdir.diameter() == 3) @@ -47,15 +47,13 @@ def testDiameter(self): self.assertTrue(d == (13, 6, 15) or d == (6, 13, 15)) def testEccentricity(self): - self.assertEqual(self.gfull.eccentricity(), - [1] * self.gfull.vcount()) - self.assertEqual(self.gempty.eccentricity(), - [0] * self.gempty.vcount()) + self.assertEqual(self.gfull.eccentricity(), [1] * self.gfull.vcount()) + self.assertEqual(self.gempty.eccentricity(), [0] * self.gempty.vcount()) self.assertEqual(self.g.eccentricity(), [1, 1, 2, 2]) - self.assertEqual(self.gdir.eccentricity(), - [1, 2, 3, 2]) - self.assertEqual(self.tree.eccentricity(), - [3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5]) + self.assertEqual(self.gdir.eccentricity(), [1, 2, 3, 2]) + self.assertEqual( + self.tree.eccentricity(), [3, 3, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5] + ) self.assertEqual(Graph().eccentricity(), []) def testRadius(self): @@ -72,51 +70,59 @@ def testTransitivity(self): self.assertTrue(self.g.transitivity_undirected() == 0.75) def testLocalTransitivity(self): - self.assertTrue(self.gfull.transitivity_local_undirected() == - [1.0] * self.gfull.vcount()) - self.assertTrue(self.tree.transitivity_local_undirected(mode="zero") == - [0.0] * self.tree.vcount()) + self.assertTrue( + self.gfull.transitivity_local_undirected() == [1.0] * self.gfull.vcount() + ) + self.assertTrue( + self.tree.transitivity_local_undirected(mode="zero") + == [0.0] * self.tree.vcount() + ) l = self.g.transitivity_local_undirected(mode="zero") - self.assertAlmostEqual(2/3, l[0], places=4) - self.assertAlmostEqual(2/3, l[1], places=4) + self.assertAlmostEqual(2 / 3, l[0], places=4) + self.assertAlmostEqual(2 / 3, l[1], places=4) self.assertEqual(1, l[2]) self.assertEqual(1, l[3]) g = Graph.Full(4) + 1 + [(0, 4)] g.es["weight"] = [1, 1, 1, 1, 1, 1, 5] self.assertAlmostEqual( - g.transitivity_local_undirected(0, weights="weight"), - 0.25, places=4) + g.transitivity_local_undirected(0, weights="weight"), 0.25, places=4 + ) def testAvgLocalTransitivity(self): self.assertTrue(self.gfull.transitivity_avglocal_undirected() == 1.0) self.assertTrue(self.tree.transitivity_avglocal_undirected() == 0.0) - self.assertAlmostEqual(self.g.transitivity_avglocal_undirected(), 5/6., places=4) + self.assertAlmostEqual( + self.g.transitivity_avglocal_undirected(), 5 / 6.0, places=4 + ) def testModularity(self): - g = Graph.Full(5)+Graph.Full(5) - g.add_edges([(0,5)]) - cl = [0]*5+[1]*5 + g = Graph.Full(5) + Graph.Full(5) + g.add_edges([(0, 5)]) + cl = [0] * 5 + [1] * 5 self.assertAlmostEqual(g.modularity(cl), 0.4523, places=3) - ws = [1]*21 + ws = [1] * 21 self.assertAlmostEqual(g.modularity(cl, ws), 0.4523, places=3) - ws = [2]*21 + ws = [2] * 21 self.assertAlmostEqual(g.modularity(cl, ws), 0.4523, places=3) - ws = [2]*10+[1]*11 + ws = [2] * 10 + [1] * 11 self.assertAlmostEqual(g.modularity(cl, ws), 0.4157, places=3) self.assertRaises(InternalError, g.modularity, cl, ws[0:20]) + class DegreeTests(unittest.TestCase): - gfull = Graph.Full(10) + gfull = Graph.Full(10) gempty = Graph(10) g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3), (0, 0)]) - gdir = Graph(4, [(0, 1), (0, 2), (1, 2), (2, 1), (0, 3), (1, 3), (3, 0)], directed=True) + gdir = Graph( + 4, [(0, 1), (0, 2), (1, 2), (2, 1), (0, 3), (1, 3), (3, 0)], directed=True + ) tree = Graph.Tree(10, 3) def testKnn(self): knn, knnk = self.gfull.knn() - self.assertTrue(knn == [9.] * 10) + self.assertTrue(knn == [9.0] * 10) self.assertAlmostEqual(knnk[8], 9.0, places=6) # knn works for simple graphs only -- self.g is not simple @@ -127,11 +133,11 @@ def testKnn(self): g.simplify() knn, knnk = g.knn() - diff = max(abs(a-b) for a, b in zip(knn, [7/3., 7/3., 3, 3])) - self.assertAlmostEqual(diff, 0., places=6) + diff = max(abs(a - b) for a, b in zip(knn, [7 / 3.0, 7 / 3.0, 3, 3])) + self.assertAlmostEqual(diff, 0.0, places=6) self.assertEqual(len(knnk), 3) self.assertAlmostEqual(knnk[1], 3, places=6) - self.assertAlmostEqual(knnk[2], 7/3., places=6) + self.assertAlmostEqual(knnk[2], 7 / 3.0, places=6) def testDegree(self): self.assertTrue(self.gfull.degree() == [9] * 10) @@ -158,8 +164,10 @@ def testMaxDegree(self): def testStrength(self): # Turn off warnings about calling strength without weights import warnings - warnings.filterwarnings("ignore", "No edge weights for strength calculation", \ - RuntimeWarning) + + warnings.filterwarnings( + "ignore", "No edge weights for strength calculation", RuntimeWarning + ) # No weights self.assertTrue(self.gfull.strength() == [9] * 10) @@ -168,28 +176,21 @@ def testStrength(self): self.assertTrue(self.g.degree() == [5, 3, 2, 2]) # With weights ws = [1, 2, 3, 4, 5, 6] - self.assertTrue(self.g.strength(weights=ws, loops=False) == \ - [7, 9, 5, 9]) + self.assertTrue(self.g.strength(weights=ws, loops=False) == [7, 9, 5, 9]) self.assertTrue(self.g.strength(weights=ws) == [19, 9, 5, 9]) ws = [1, 2, 3, 4, 5, 6, 7] - self.assertTrue(self.gdir.strength(mode=IN, weights=ws) == \ - [7, 5, 5, 11]) - self.assertTrue(self.gdir.strength(mode=OUT, weights=ws) == \ - [8, 9, 4, 7]) - self.assertTrue(self.gdir.strength(mode=ALL, weights=ws) == \ - [15, 14, 9, 18]) + self.assertTrue(self.gdir.strength(mode=IN, weights=ws) == [7, 5, 5, 11]) + self.assertTrue(self.gdir.strength(mode=OUT, weights=ws) == [8, 9, 4, 7]) + self.assertTrue(self.gdir.strength(mode=ALL, weights=ws) == [15, 14, 9, 18]) vs = self.gdir.vs.select(0, 2) - self.assertTrue(self.gdir.strength(vs, mode=ALL, weights=ws) == \ - [15, 9]) - self.assertTrue(self.gdir.strength(self.gdir.vs[1], \ - mode=ALL, weights=ws) == 14) - + self.assertTrue(self.gdir.strength(vs, mode=ALL, weights=ws) == [15, 9]) + self.assertTrue(self.gdir.strength(self.gdir.vs[1], mode=ALL, weights=ws) == 14) class LocalTransitivityTests(unittest.TestCase): def testLocalTransitivityFull(self): trans = Graph.Full(10).transitivity_local_undirected() - self.assertTrue(trans == [1.0]*10) + self.assertTrue(trans == [1.0] * 10) def testLocalTransitivityTree(self): trans = Graph.Tree(10, 3).transitivity_local_undirected() @@ -203,61 +204,65 @@ def testLocalTransitivityHalf(self): def testLocalTransitivityPartial(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) - trans = g.transitivity_local_undirected([1,2]) + trans = g.transitivity_local_undirected([1, 2]) trans = [round(x, 3) for x in trans] self.assertTrue(trans == [0.667, 1.0]) class BiconnectedComponentTests(unittest.TestCase): g1 = Graph.Full(10) - g2 = Graph(5, [(0,1),(1,2),(2,3),(3,4)]) - g3 = Graph(6, [(0,1),(1,2),(2,3),(3,0),(2,4),(2,5),(4,5)]) + g2 = Graph(5, [(0, 1), (1, 2), (2, 3), (3, 4)]) + g3 = Graph(6, [(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (2, 5), (4, 5)]) def testBiconnectedComponents(self): s = self.g1.biconnected_components() - self.assertTrue(len(s) == 1 and s[0]==list(range(10))) + self.assertTrue(len(s) == 1 and s[0] == list(range(10))) s, ap = self.g1.biconnected_components(True) - self.assertTrue(len(s) == 1 and s[0]==list(range(10))) + self.assertTrue(len(s) == 1 and s[0] == list(range(10))) s = self.g3.biconnected_components() - self.assertTrue(len(s) == 2 and s[0]==[2,4,5] and s[1]==[0,1,2,3]) + self.assertTrue(len(s) == 2 and s[0] == [2, 4, 5] and s[1] == [0, 1, 2, 3]) s, ap = self.g3.biconnected_components(True) - self.assertTrue(len(s) == 2 and s[0]==[2,4,5] and \ - s[1]==[0,1,2,3] and ap == [2]) + self.assertTrue( + len(s) == 2 and s[0] == [2, 4, 5] and s[1] == [0, 1, 2, 3] and ap == [2] + ) def testArticulationPoints(self): self.assertTrue(self.g1.articulation_points() == []) - self.assertTrue(self.g2.cut_vertices() == [1,2,3]) + self.assertTrue(self.g2.cut_vertices() == [1, 2, 3]) self.assertTrue(self.g3.articulation_points() == [2]) class CentralityTests(unittest.TestCase): def testBetweennessCentrality(self): g = Graph.Star(5) - self.assertTrue(g.betweenness() == [6., 0., 0., 0., 0.]) + self.assertTrue(g.betweenness() == [6.0, 0.0, 0.0, 0.0, 0.0]) g = Graph(5, [(0, 1), (0, 2), (0, 3), (1, 4)]) - self.assertTrue(g.betweenness() == [5., 3., 0., 0., 0.]) - self.assertTrue(g.betweenness(cutoff=2) == [3., 1., 0., 0., 0.]) - self.assertTrue(g.betweenness(cutoff=1) == [0., 0., 0., 0., 0.]) + self.assertTrue(g.betweenness() == [5.0, 3.0, 0.0, 0.0, 0.0]) + self.assertTrue(g.betweenness(cutoff=2) == [3.0, 1.0, 0.0, 0.0, 0.0]) + self.assertTrue(g.betweenness(cutoff=1) == [0.0, 0.0, 0.0, 0.0, 0.0]) g = Graph.Lattice([3, 3], circular=False) - self.assertTrue(g.betweenness(cutoff=2) == [0.5, 2.0, 0.5, 2.0, 4.0, 2.0, 0.5, 2.0, 0.5]) + self.assertTrue( + g.betweenness(cutoff=2) == [0.5, 2.0, 0.5, 2.0, 4.0, 2.0, 0.5, 2.0, 0.5] + ) def testEdgeBetweennessCentrality(self): g = Graph.Star(5) - self.assertTrue(g.edge_betweenness() == [4., 4., 4., 4.]) + self.assertTrue(g.edge_betweenness() == [4.0, 4.0, 4.0, 4.0]) g = Graph(5, [(0, 1), (0, 2), (0, 3), (1, 4)]) - self.assertTrue(g.edge_betweenness() == [6., 4., 4., 4.]) - self.assertTrue(g.edge_betweenness(cutoff=2) == [4., 3., 3., 2.]) - self.assertTrue(g.edge_betweenness(cutoff=1) == [1., 1., 1., 1.]) + self.assertTrue(g.edge_betweenness() == [6.0, 4.0, 4.0, 4.0]) + self.assertTrue(g.edge_betweenness(cutoff=2) == [4.0, 3.0, 3.0, 2.0]) + self.assertTrue(g.edge_betweenness(cutoff=1) == [1.0, 1.0, 1.0, 1.0]) g = Graph.Ring(5) - self.assertTrue(g.edge_betweenness() == [3., 3., 3., 3., 3.]) - self.assertTrue(g.edge_betweenness(weights=[4, 1, 1, 1, 1]) == \ - [0.5, 3.5, 5.5, 5.5, 3.5]) + self.assertTrue(g.edge_betweenness() == [3.0, 3.0, 3.0, 3.0, 3.0]) + self.assertTrue( + g.edge_betweenness(weights=[4, 1, 1, 1, 1]) == [0.5, 3.5, 5.5, 5.5, 3.5] + ) def testClosenessCentrality(self): g = Graph.Star(5) cl = g.closeness() - cl2 = [1., 0.57142, 0.57142, 0.57142, 0.57142] + cl2 = [1.0, 0.57142, 0.57142, 0.57142, 0.57142] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -265,7 +270,7 @@ def testClosenessCentrality(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.closeness(cutoff=1) - cl2 = [1., 0.25, 0.25, 0.25, 0.25] + cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -273,7 +278,7 @@ def testClosenessCentrality(self): g = Graph.Star(5) cl = g.closeness(weights=weights) - cl2 = [1., 0.57142, 0.57142, 0.57142, 0.57142] + cl2 = [1.0, 0.57142, 0.57142, 0.57142, 0.57142] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -281,25 +286,72 @@ def testClosenessCentrality(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.closeness(cutoff=1, weights=weights) - cl2 = [1., 0.25, 0.25, 0.25, 0.25] + cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) # Test for igraph/igraph:#1078 - g = Graph([ - (0, 1), (0, 2), (0, 5), (0, 6), (0, 9), (1, 6), (1, 8), - (2, 4), (2, 6), (2, 7), (2, 8), (3, 6), (4, 8), - (5, 6), (5, 9), (6, 7), (6, 8), (7, 8), (7, 9), (8, 9) - ]) - weights = [0.69452, 0.329886, 0.131649, 0.503269, 0.472738, - 0.370933, 0.23857, 0.0354043, 0.189015, 0.355118, 0.768335, - 0.893289, 0.891709, 0.494896, 0.924684, 0.432001, 0.858159, - 0.246798, 0.881304, 0.64685] + g = Graph( + [ + (0, 1), + (0, 2), + (0, 5), + (0, 6), + (0, 9), + (1, 6), + (1, 8), + (2, 4), + (2, 6), + (2, 7), + (2, 8), + (3, 6), + (4, 8), + (5, 6), + (5, 9), + (6, 7), + (6, 8), + (7, 8), + (7, 9), + (8, 9), + ] + ) + weights = [ + 0.69452, + 0.329886, + 0.131649, + 0.503269, + 0.472738, + 0.370933, + 0.23857, + 0.0354043, + 0.189015, + 0.355118, + 0.768335, + 0.893289, + 0.891709, + 0.494896, + 0.924684, + 0.432001, + 0.858159, + 0.246798, + 0.881304, + 0.64685, + ] with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.closeness(weights=weights) - expected_cl = [1.63318, 1.52014, 2.03724, 0.760158, 1.91449, - 1.43224, 1.91761, 1.60198, 1.3891, 1.12829] + expected_cl = [ + 1.63318, + 1.52014, + 2.03724, + 0.760158, + 1.91449, + 1.43224, + 1.91761, + 1.60198, + 1.3891, + 1.12829, + ] for obs, exp in zip(cl, expected_cl): self.assertAlmostEqual(obs, exp, places=4) @@ -311,14 +363,14 @@ def testPageRank(self): def testPersonalizedPageRank(self): g = Graph.Star(11) - self.assertRaises(InternalError, g.personalized_pagerank, reset=[0]*11) - cent = g.personalized_pagerank(reset=[0,10]+[0]*9, damping=0.5) + self.assertRaises(InternalError, g.personalized_pagerank, reset=[0] * 11) + cent = g.personalized_pagerank(reset=[0, 10] + [0] * 9, damping=0.5) self.assertTrue(cent.index(max(cent)) == 1) self.assertAlmostEqual(cent[0], 0.3333, places=3) self.assertAlmostEqual(cent[1], 0.5166, places=3) self.assertAlmostEqual(cent[2], 0.0166, places=3) cent2 = g.personalized_pagerank(reset_vertices=g.vs[1], damping=0.5) - self.assertTrue(max(abs(x-y) for x, y in zip(cent, cent2)) < 0.001) + self.assertTrue(max(abs(x - y) for x, y in zip(cent, cent2)) < 0.001) def testEigenvectorCentrality(self): g = Graph.Star(11) @@ -327,9 +379,10 @@ def testEigenvectorCentrality(self): self.assertAlmostEqual(max(cent), 1.0, places=3) self.assertTrue(min(cent) >= 0) cent, ev = g.evcent(scale=False, return_eigenvalue=True) - if cent[0]<0: cent = [-x for x in cent] + if cent[0] < 0: + cent = [-x for x in cent] self.assertTrue(cent.index(max(cent)) == 0) - self.assertAlmostEqual(cent[1]/cent[0], 0.3162, places=3) + self.assertAlmostEqual(cent[1] / cent[0], 0.3162, places=3) self.assertAlmostEqual(ev, 3.162, places=3) def testAuthorityScore(self): @@ -337,41 +390,78 @@ def testAuthorityScore(self): asc = g.authority_score() self.assertAlmostEqual(max(asc), 1.0, places=3) asc, ev = g.hub_score(scale=False, return_eigenvalue=True) - if asc[0]<0: hs = [-x for x in asc] + if asc[0] < 0: + hs = [-x for x in asc] def testHubScore(self): g = Graph.Tree(15, 2, TREE_IN) hsc = g.hub_score() self.assertAlmostEqual(max(hsc), 1.0, places=3) hsc, ev = g.hub_score(scale=False, return_eigenvalue=True) - if hsc[0]<0: hsc = [-x for x in hsc] + if hsc[0] < 0: + hsc = [-x for x in hsc] def testCoreness(self): - g = Graph.Full(4) + Graph(4) + [(0,4), (1,5), (2,6), (3,7)] - self.assertEqual(g.coreness("A"), [3,3,3,3,1,1,1,1]) + g = Graph.Full(4) + Graph(4) + [(0, 4), (1, 5), (2, 6), (3, 7)] + self.assertEqual(g.coreness("A"), [3, 3, 3, 3, 1, 1, 1, 1]) class NeighborhoodTests(unittest.TestCase): def testNeighborhood(self): g = Graph.Ring(10, circular=False) - self.assertTrue(list(map(sorted, g.neighborhood())) == \ - [[0,1], [0,1,2], [1,2,3], [2,3,4], [3,4,5], [4,5,6], \ - [5,6,7], [6,7,8], [7,8,9], [8,9]]) - self.assertTrue(list(map(sorted, g.neighborhood(order=3))) == \ - [[0,1,2,3], [0,1,2,3,4], [0,1,2,3,4,5], [0,1,2,3,4,5,6], \ - [1,2,3,4,5,6,7], [2,3,4,5,6,7,8], [3,4,5,6,7,8,9], \ - [4,5,6,7,8,9], [5,6,7,8,9], [6,7,8,9]]) - self.assertTrue(list(map(sorted, g.neighborhood(order=3, mindist=2))) == \ - [[2,3], [3,4], [0,4,5], [0,1,5,6], \ - [1,2,6,7], [2,3,7,8], [3,4,8,9], \ - [4,5,9], [5,6], [6,7]]) + self.assertTrue( + list(map(sorted, g.neighborhood())) + == [ + [0, 1], + [0, 1, 2], + [1, 2, 3], + [2, 3, 4], + [3, 4, 5], + [4, 5, 6], + [5, 6, 7], + [6, 7, 8], + [7, 8, 9], + [8, 9], + ] + ) + self.assertTrue( + list(map(sorted, g.neighborhood(order=3))) + == [ + [0, 1, 2, 3], + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5, 6], + [1, 2, 3, 4, 5, 6, 7], + [2, 3, 4, 5, 6, 7, 8], + [3, 4, 5, 6, 7, 8, 9], + [4, 5, 6, 7, 8, 9], + [5, 6, 7, 8, 9], + [6, 7, 8, 9], + ] + ) + self.assertTrue( + list(map(sorted, g.neighborhood(order=3, mindist=2))) + == [ + [2, 3], + [3, 4], + [0, 4, 5], + [0, 1, 5, 6], + [1, 2, 6, 7], + [2, 3, 7, 8], + [3, 4, 8, 9], + [4, 5, 9], + [5, 6], + [6, 7], + ] + ) def testNeighborhoodSize(self): g = Graph.Ring(10, circular=False) - self.assertTrue(g.neighborhood_size() == [2,3,3,3,3,3,3,3,3,2]) - self.assertTrue(g.neighborhood_size(order=3) == [4,5,6,7,7,7,7,6,5,4]) - self.assertTrue(g.neighborhood_size(order=3, mindist=2) == \ - [2,2,3,4,4,4,4,3,2,2]) + self.assertTrue(g.neighborhood_size() == [2, 3, 3, 3, 3, 3, 3, 3, 3, 2]) + self.assertTrue(g.neighborhood_size(order=3) == [4, 5, 6, 7, 7, 7, 7, 6, 5, 4]) + self.assertTrue( + g.neighborhood_size(order=3, mindist=2) == [2, 2, 3, 4, 4, 4, 4, 3, 2, 2] + ) class MiscTests(unittest.TestCase): @@ -382,10 +472,10 @@ def testBridges(self): self.assertTrue(g.bridges() == [3]) g = Graph(3, [(0, 1), (1, 2), (2, 3)]) self.assertTrue(g.bridges() == [0, 1, 2]) - + def testConstraint(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) - self.assertTrue(isinstance(g.constraint(), list)) # TODO check more + self.assertTrue(isinstance(g.constraint(), list)) # TODO check more def testTopologicalSorting(self): g = Graph(5, [(0, 1), (0, 2), (1, 2), (1, 3), (2, 3)], directed=True) @@ -410,7 +500,9 @@ def testLineGraph(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) el = g.linegraph().get_edgelist() el.sort() - self.assertTrue(el == [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (2, 4), (3, 4)]) + self.assertTrue( + el == [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (2, 4), (3, 4)] + ) g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)], directed=True) el = g.linegraph().get_edgelist() @@ -420,31 +512,56 @@ def testLineGraph(self): class PathTests(unittest.TestCase): def testShortestPaths(self): - g = Graph(10, [(0,1), (0,2), (0,3), (1,2), (1,4), (1,5), (2,3), (2,6), \ - (3,2), (3,6), (4,5), (4,7), (5,6), (5,8), (5,9), (7,5), (7,8), \ - (8,9), (5,2), (2,1)], directed=True) - ws = [0,2,1,0,5,2,1,1,0,2,2,8,1,1,3,1,1,4,2,1] + g = Graph( + 10, + [ + (0, 1), + (0, 2), + (0, 3), + (1, 2), + (1, 4), + (1, 5), + (2, 3), + (2, 6), + (3, 2), + (3, 6), + (4, 5), + (4, 7), + (5, 6), + (5, 8), + (5, 9), + (7, 5), + (7, 8), + (8, 9), + (5, 2), + (2, 1), + ], + directed=True, + ) + ws = [0, 2, 1, 0, 5, 2, 1, 1, 0, 2, 2, 8, 1, 1, 3, 1, 1, 4, 2, 1] g.es["weight"] = ws - inf = float('inf') + inf = float("inf") expected = [ - [0, 0, 0, 1, 5, 2, 1, 13, 3, 5], - [inf, 0, 0, 1, 5, 2, 1, 13, 3, 5], - [inf, 1, 0, 1, 6, 3, 1, 14, 4, 6], - [inf, 1, 0, 0, 6, 3, 1, 14, 4, 6], - [inf, 5, 4, 5, 0, 2, 3, 8, 3, 5], - [inf, 3, 2, 3, 8, 0, 1, 16, 1, 3], - [inf, inf, inf, inf, inf, inf, 0, inf, inf, inf], - [inf, 4, 3, 4, 9, 1, 2, 0, 1, 4], - [inf, inf, inf, inf, inf, inf, inf, inf, 0, 4], - [inf, inf, inf, inf, inf, inf, inf, inf, inf, 0] + [0, 0, 0, 1, 5, 2, 1, 13, 3, 5], + [inf, 0, 0, 1, 5, 2, 1, 13, 3, 5], + [inf, 1, 0, 1, 6, 3, 1, 14, 4, 6], + [inf, 1, 0, 0, 6, 3, 1, 14, 4, 6], + [inf, 5, 4, 5, 0, 2, 3, 8, 3, 5], + [inf, 3, 2, 3, 8, 0, 1, 16, 1, 3], + [inf, inf, inf, inf, inf, inf, 0, inf, inf, inf], + [inf, 4, 3, 4, 9, 1, 2, 0, 1, 4], + [inf, inf, inf, inf, inf, inf, inf, inf, 0, 4], + [inf, inf, inf, inf, inf, inf, inf, inf, inf, 0], ] self.assertTrue(g.shortest_paths(weights=ws) == expected) self.assertTrue(g.shortest_paths(weights="weight") == expected) - self.assertTrue(g.shortest_paths(weights="weight", target=[2,3]) == - [row[2:4] for row in expected]) + self.assertTrue( + g.shortest_paths(weights="weight", target=[2, 3]) + == [row[2:4] for row in expected] + ) def testGetShortestPaths(self): - g = Graph(4, [(0,1), (0,2), (1,3), (3,2), (2,1)], directed=True) + g = Graph(4, [(0, 1), (0, 2), (1, 3), (3, 2), (2, 1)], directed=True) sps = g.get_shortest_paths(0) expected = [[0], [0, 1], [0, 2], [0, 1, 3]] self.assertTrue(sps == expected) @@ -457,7 +574,7 @@ def testGetShortestPaths(self): self.assertRaises(ValueError, g.get_shortest_paths, 0, output="x") def testGetAllShortestPaths(self): - g = Graph(4, [(0,1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)], directed=True) + g = Graph(4, [(0, 1), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)], directed=True) sps = sorted(g.get_all_shortest_paths(0, 0)) expected = [[0]] @@ -474,28 +591,40 @@ def testGetAllShortestPaths(self): g = Graph.Lattice([5, 5], circular=False) sps = sorted(g.get_all_shortest_paths(0, 12)) - expected = [[0, 1, 2, 7, 12], [0, 1, 6, 7, 12], [0, 1, 6, 11, 12], \ - [0, 5, 6, 7, 12], [0, 5, 6, 11, 12], [0, 5, 10, 11, 12]] + expected = [ + [0, 1, 2, 7, 12], + [0, 1, 6, 7, 12], + [0, 1, 6, 11, 12], + [0, 5, 6, 7, 12], + [0, 5, 6, 11, 12], + [0, 5, 10, 11, 12], + ] self.assertEqual(expected, sps) g = Graph.Lattice([100, 100], circular=False) sps = sorted(g.get_all_shortest_paths(0, 202)) - expected = [[0, 1, 2, 102, 202], [0, 1, 101, 102, 202], [0, 1, 101, 201, 202], \ - [0, 100, 101, 102, 202], [0, 100, 101, 201, 202], [0, 100, 200, 201, 202]] + expected = [ + [0, 1, 2, 102, 202], + [0, 1, 101, 102, 202], + [0, 1, 101, 201, 202], + [0, 100, 101, 102, 202], + [0, 100, 101, 201, 202], + [0, 100, 200, 201, 202], + ] self.assertEqual(expected, sps) g = Graph.Lattice([100, 100], circular=False) sps = sorted(g.get_all_shortest_paths(0, [0, 202])) self.assertEqual([[0]] + expected, sps) - g = Graph([(0,1), (1,2), (0,2)]) + g = Graph([(0, 1), (1, 2), (0, 2)]) g.es["weight"] = [0.5, 0.5, 1] sps = sorted(g.get_all_shortest_paths(0, weights="weight")) - self.assertEqual([[0], [0,1], [0,1,2], [0,2]], sps) + self.assertEqual([[0], [0, 1], [0, 1, 2], [0, 2]], sps) g = Graph.Lattice([4, 4], circular=False) g.es["weight"] = 1 - g.es[2,8]["weight"] = 100 + g.es[2, 8]["weight"] = 100 sps = sorted(g.get_all_shortest_paths(0, [3, 12, 15], weights="weight")) self.assertEqual(20, len(sps)) self.assertEqual(4, sum(1 for path in sps if path[-1] == 3)) @@ -505,21 +634,27 @@ def testGetAllShortestPaths(self): def testGetAllSimplePaths(self): g = Graph.Ring(20) sps = sorted(g.get_all_simple_paths(0, 10)) - self.assertEqual([ - [0,1,2,3,4,5,6,7,8,9,10], - [0,19,18,17,16,15,14,13,12,11,10] - ], sps) + self.assertEqual( + [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [0, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10], + ], + sps, + ) g = Graph.Ring(20, directed=True) sps = sorted(g.get_all_simple_paths(0, 10)) - self.assertEqual([ [0,1,2,3,4,5,6,7,8,9,10] ], sps) + self.assertEqual([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]], sps) sps = sorted(g.get_all_simple_paths(0, 10, mode="in")) - self.assertEqual([ [0,19,18,17,16,15,14,13,12,11,10] ], sps) + self.assertEqual([[0, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10]], sps) sps = sorted(g.get_all_simple_paths(0, 10, mode="all")) - self.assertEqual([ - [0,1,2,3,4,5,6,7,8,9,10], - [0,19,18,17,16,15,14,13,12,11,10] - ], sps) + self.assertEqual( + [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + [0, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10], + ], + sps, + ) g = Graph.Lattice([4, 4], circular=False) g = Graph([(min(u, v), max(u, v)) for u, v in g.get_edgelist()], directed=True) @@ -537,9 +672,11 @@ def testPathLengthHist(self): g = Graph.Tree(15, 2) h = g.path_length_hist() self.assertTrue(h.unconnected == 0) - self.assertTrue([(int(l),x) for l,_,x in h.bins()] == \ - [(1,14),(2,19),(3,20),(4,20),(5,16),(6,16)]) - g = Graph.Full(5)+Graph.Full(4) + self.assertTrue( + [(int(l), x) for l, _, x in h.bins()] + == [(1, 14), (2, 19), (3, 20), (4, 20), (5, 16), (6, 16)] + ) + g = Graph.Full(5) + Graph.Full(4) h = g.path_length_hist() self.assertTrue(h.unconnected == 20) g.to_directed() @@ -548,11 +685,12 @@ def testPathLengthHist(self): h = g.path_length_hist(False) self.assertTrue(h.unconnected == 20) + class DominatorTests(unittest.TestCase): def compareDomTrees(self, alist, blist): - ''' + """ Required due to NaN use for isolated nodes - ''' + """ if len(alist) != len(blist): return False for i, (a, b) in enumerate(zip(alist, blist)): @@ -568,28 +706,119 @@ def testDominators(self): # examples taken from igraph's examples/simple/dominator_tree.out # initial - g = Graph(13, [(0,1), (0,7), (0,10), (1,2), (1,5), (2,3), (3,4), (4,3), - (4,0), (5,3), (5,6), (6,3), (7,8), (7,10), (7,11), (8,9), (9,4), - (9,8), (10,11), (11,12), (12,9) ], directed=True) - s = [-1, 0, 1, 0, 0, 1, 5, 0, 0, 0, 0, 0, 11 ] + g = Graph( + 13, + [ + (0, 1), + (0, 7), + (0, 10), + (1, 2), + (1, 5), + (2, 3), + (3, 4), + (4, 3), + (4, 0), + (5, 3), + (5, 6), + (6, 3), + (7, 8), + (7, 10), + (7, 11), + (8, 9), + (9, 4), + (9, 8), + (10, 11), + (11, 12), + (12, 9), + ], + directed=True, + ) + s = [-1, 0, 1, 0, 0, 1, 5, 0, 0, 0, 0, 0, 11] r = g.dominator(0) self.assertTrue(self.compareDomTrees(s, r)) # flipped edges - g = Graph(13, [(1,0), (2,0), (3,0), (4,1), (1,2), (4,2), (5,2), (6,3), - (7,3), (12,4), (8,5), (9,6), (9,7), (10,7), (5,8), (11,8), - (11,9), (9,10), (9,11), (0,11), (8,12)], directed=True) + g = Graph( + 13, + [ + (1, 0), + (2, 0), + (3, 0), + (4, 1), + (1, 2), + (4, 2), + (5, 2), + (6, 3), + (7, 3), + (12, 4), + (8, 5), + (9, 6), + (9, 7), + (10, 7), + (5, 8), + (11, 8), + (11, 9), + (9, 10), + (9, 11), + (0, 11), + (8, 12), + ], + directed=True, + ) s = [-1, 0, 0, 0, 0, 0, 3, 3, 0, 0, 7, 0, 4] r = g.dominator(0, mode=IN) self.assertTrue(self.compareDomTrees(s, r)) # disconnected components - g = Graph(20, [(0,1), (0,2), (0,3), (1,4), (2,1), (2,4), (2,8), (3,9), - (3,10), (4,15), (8,11), (9,12), (10,12), (10,13), (11,8), - (11,14), (12,14), (13,12), (14,12), (14,0), (15,11)], - directed=True) - s = [-1, 0, 0, 0, 0, float("nan"), float("nan"), float("nan"), 0, 3, 3, 0, - 0, 10, 0, 4, float("nan"), float("nan"), float("nan"), float("nan")] + g = Graph( + 20, + [ + (0, 1), + (0, 2), + (0, 3), + (1, 4), + (2, 1), + (2, 4), + (2, 8), + (3, 9), + (3, 10), + (4, 15), + (8, 11), + (9, 12), + (10, 12), + (10, 13), + (11, 8), + (11, 14), + (12, 14), + (13, 12), + (14, 12), + (14, 0), + (15, 11), + ], + directed=True, + ) + s = [ + -1, + 0, + 0, + 0, + 0, + float("nan"), + float("nan"), + float("nan"), + 0, + 3, + 3, + 0, + 0, + 10, + 0, + 4, + float("nan"), + float("nan"), + float("nan"), + float("nan"), + ] r = g.dominator(0, mode=OUT) self.assertTrue(self.compareDomTrees(s, r)) @@ -604,20 +833,25 @@ def suite(): path_suite = unittest.makeSuite(PathTests) misc_suite = unittest.makeSuite(MiscTests) dominator_suite = unittest.makeSuite(DominatorTests) - return unittest.TestSuite([simple_suite, - degree_suite, - local_transitivity_suite, - biconnected_suite, - centrality_suite, - neighborhood_suite, - path_suite, - misc_suite, - dominator_suite]) + return unittest.TestSuite( + [ + simple_suite, + degree_suite, + local_transitivity_suite, + biconnected_suite, + centrality_suite, + neighborhood_suite, + path_suite, + misc_suite, + dominator_suite, + ] + ) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) + if __name__ == "__main__": test() - diff --git a/tests/test_unicode_issues.py b/tests/test_unicode_issues.py index 3dccdc48b..543f871e5 100644 --- a/tests/test_unicode_issues.py +++ b/tests/test_unicode_issues.py @@ -1,27 +1,27 @@ -from __future__ import unicode_literals - import unittest from igraph import Graph + class UnicodeTests(unittest.TestCase): def testBug128(self): y = [1, 4, 9] - g = Graph(n=len(y), directed=True, vertex_attrs={'y': y}) + g = Graph(n=len(y), directed=True, vertex_attrs={"y": y}) self.assertEqual(3, g.vcount()) g.add_vertices(1) # Bug #128 would prevent us from reaching the next statement # because an exception would have been thrown here self.assertEqual(4, g.vcount()) - + def suite(): generator_suite = unittest.makeSuite(UnicodeTests) return unittest.TestSuite([generator_suite]) + def test(): runner = unittest.TextTestRunner() runner.run(suite()) - + + if __name__ == "__main__": test() - diff --git a/tests/test_vertexseq.py b/tests/test_vertexseq.py index 2d085413c..a359cc9aa 100644 --- a/tests/test_vertexseq.py +++ b/tests/test_vertexseq.py @@ -77,7 +77,7 @@ def testIncident(self): for i in range(g.vcount()): vertex = g.vs[i] - for mode, method_name in method_table.items(): + for mode, method_name in list(method_table.items()): method = getattr(vertex, method_name) self.assertEqual( g.incident(i, mode=mode), @@ -156,7 +156,7 @@ def testProxyMethods(self): class VertexSeqTests(unittest.TestCase): def setUp(self): self.g = Graph.Full(10) - self.g.vs["test"] = range(10) + self.g.vs["test"] = list(range(10)) self.g.vs["name"] = list("ABCDEFGHIJ") def testCreation(self): @@ -204,7 +204,7 @@ def testPartialAttributeAssignment(self): only_even = self.g.vs.select(lambda v: (v.index % 2 == 0)) only_even["test"] = [0] * len(only_even) self.assertTrue(self.g.vs["test"] == [0, 1, 0, 3, 0, 5, 0, 7, 0, 9]) - only_even["test2"] = range(5) + only_even["test2"] = list(range(5)) self.assertTrue( self.g.vs["test2"] == [0, None, 1, None, 2, None, 3, None, 4, None] ) @@ -293,7 +293,7 @@ def testStringFilteringFind(self): self.assertRaises(ValueError, self.g.vs.find, "NoSuchName") def testIterableFilteringSelect(self): - subset = self.g.vs.select(range(5, 8)) + subset = self.g.vs.select(list(range(5, 8))) self.assertTrue(len(subset) == 3) self.assertTrue(subset["test"] == [5, 6, 7]) diff --git a/tests/test_walks.py b/tests/test_walks.py index 8386bdecd..51a67de4e 100644 --- a/tests/test_walks.py +++ b/tests/test_walks.py @@ -16,7 +16,7 @@ def validate_walk(self, g, walk, start, length, mode="out"): def testRandomWalkUndirected(self): g = Graph.GRG(100, 0.2) for i in range(100): - start = random.randint(0, g.vcount()-1) + start = random.randint(0, g.vcount() - 1) length = random.randint(0, 10) walk = g.random_walk(start, length) self.validate_walk(g, walk, start, length) @@ -34,7 +34,7 @@ def testRandomWalkDirectedIn(self): g = Graph.Tree(121, 3, mode="out") mode = "in" for i in range(100): - start = random.randint(40, g.vcount()-1) + start = random.randint(40, g.vcount() - 1) length = random.randint(0, 4) walk = g.random_walk(start, length, mode) self.validate_walk(g, walk, start, length, mode) @@ -43,7 +43,7 @@ def testRandomWalkDirectedAll(self): g = Graph.Tree(121, 3, mode="out") mode = "all" for i in range(100): - start = random.randint(0, g.vcount()-1) + start = random.randint(0, g.vcount() - 1) length = random.randint(0, 10) walk = g.random_walk(start, length, mode) self.validate_walk(g, walk, start, length, mode) diff --git a/tests/utils.py b/tests/utils.py index 82b951a54..8bfbc88d4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,14 +16,17 @@ def _id(obj): return obj + try: from unittest import skip except ImportError: # Provide basic replacement for unittest.skip def skip(reason): """Unconditionally skip a test.""" + def decorator(test_item): - if not isinstance(test_item, (type, types.ClassType)): + if not isinstance(test_item, type): + @functools.wraps(test_item) def skip_wrapper(*args, **kwds): if reason: @@ -31,10 +34,13 @@ def skip_wrapper(*args, **kwds): else: sys.stderr.write("skipped, ") return + test_item = skip_wrapper return test_item + return decorator + try: from unittest import skipIf except ImportError: @@ -69,4 +75,4 @@ def temporary_file(content=None, mode=None, binary=False): os.unlink(tmpfname) -is_pypy = (platform.python_implementation() == "PyPy") +is_pypy = platform.python_implementation() == "PyPy" From 3455e2f62f6695ab18b2530d9e32444cacaa8aff Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 15 Jan 2021 21:23:29 +1100 Subject: [PATCH 0090/1681] Python 3.6 is the oldest support --- README.md | 4 +--- setup.py | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a084ce5e4..b89b58c09 100644 --- a/README.md +++ b/README.md @@ -163,9 +163,7 @@ of the non-EOL Python versions. Continuous integration tests are regularly executed on all non-EOL Python branches. -As for Python 2.x, the latest branch of `python-igraph` that supports Python 2 -is the 0.8.x series. Python 2 support will be dropped with the release of -`python-igraph` 0.9. +Python 2 support has been dropped with the release of `python-igraph` 0.9. ### PyPy diff --git a/setup.py b/setup.py index f92ddda81..df8bbbe35 100644 --- a/setup.py +++ b/setup.py @@ -7,8 +7,8 @@ ########################################################################### # Check Python's version info and exit early if it is too old -if sys.version_info < (3, 5): - print("This module requires Python >= 3.5") +if sys.version_info < (3, 6): + print("This module requires Python >= 3.6") sys.exit(0) # Check whether we are compiling for PyPy. Headers will not be installed @@ -913,10 +913,10 @@ def use_educated_guess(self): "Operating System :: OS Independent", "Programming Language :: C", "Programming Language :: Python", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Mathematics", From 41bbd12878e1e2dfbe9c96f0b79fa5baa8406ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 20 Jan 2021 10:38:02 +0100 Subject: [PATCH 0091/1681] drop PyPy2.7 from appveyor.yml --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 4e3ee3605..27bad4eb4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,13 +10,13 @@ environment: matrix: - CIBW_BUILD: "*-win32" - CIBW_SKIP: "cp27-* cp35-*" + CIBW_SKIP: "pp27-* cp27-* cp35-*" MSYSTEM: MINGW32 PATH: C:\msys64\usr\bin;C:\msys64\mingw32\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x86" - CIBW_BUILD: "*-win_amd64" - CIBW_SKIP: "cp27-* cp35-*" + CIBW_SKIP: "pp27-* cp27-* cp35-*" MSYSTEM: MINGW64 PATH: C:\msys64\usr\bin;C:\msys64\mingw64\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x64" From e30de8dab8299a4485ad9d607d3523213d9a726e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 20 Jan 2021 10:38:30 +0100 Subject: [PATCH 0092/1681] drop Python 2.7 from travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d4b4da247..8265b24c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ dist: bionic language: python python: - - "2.7" - "3.6" - "3.7" - "3.8" From 82b97c9a215ff75c40bacd6a4c02ebc3a5e929c5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Jan 2021 11:54:30 +0100 Subject: [PATCH 0093/1681] fix: changed default splitting heuristics of BLISS, see igraph/igraph#1170 --- src/_igraph/graphobject.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 1cc937635..535c324b7 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -8318,7 +8318,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( PyObject *sh_o = Py_None; PyObject *color_o = Py_None; PyObject *list; - igraph_bliss_sh_t sh = IGRAPH_BLISS_FM; + igraph_bliss_sh_t sh = IGRAPH_BLISS_FL; igraph_vector_t labeling; igraph_vector_int_t *color = 0; int retval; @@ -8444,7 +8444,7 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, PyObject *color1_o=Py_None, *color2_o=Py_None; igraphmodule_GraphObject *other; igraph_vector_t mapping_12, mapping_21, *map12=0, *map21=0; - igraph_bliss_sh_t sh1=IGRAPH_BLISS_FM, sh2=IGRAPH_BLISS_FM; + igraph_bliss_sh_t sh1=IGRAPH_BLISS_FL, sh2=IGRAPH_BLISS_FL; igraph_vector_int_t *color1=0, *color2=0; int retval; @@ -14745,7 +14745,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"canonical_permutation", (PyCFunction) igraphmodule_Graph_canonical_permutation, METH_VARARGS | METH_KEYWORDS, - "canonical_permutation(sh=\"fm\", color=None)\n\n" + "canonical_permutation(sh=\"fl\", color=None)\n\n" "Calculates the canonical permutation of a graph using the BLISS isomorphism\n" "algorithm.\n\n" "Passing the permutation returned here to L{Graph.permute_vertices()} will\n" @@ -14800,7 +14800,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"isomorphic_bliss", (PyCFunction) igraphmodule_Graph_isomorphic_bliss, METH_VARARGS | METH_KEYWORDS, "isomorphic_bliss(other, return_mapping_12=False, return_mapping_21=False,\n" - " sh1=\"fm\", sh2=None, color1=None, color2=None)\n\n" + " sh1=\"fl\", sh2=None, color1=None, color2=None)\n\n" "Checks whether the graph is isomorphic to another graph, using the\n" "BLISS isomorphism algorithm.\n\n" "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" From b5423257daa3793b79e3340ff859251fb27c404e Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 22 Jan 2021 09:43:39 +1100 Subject: [PATCH 0094/1681] Rebase on top of master, which abandons Python 2 --- src/igraph/__init__.py | 71 +++++++++++++++++++++++++++++++++++++- tests/test_foreign.py | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index c87bfa415..6f05e44ac 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -53,7 +53,6 @@ import operator from collections import defaultdict - from shutil import copyfileobj from warnings import warn @@ -3410,6 +3409,76 @@ def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): return g + def get_vertex_dataframe(self): + """Export vertices with attributes to pandas.DataFrame + + @return: a pandas.DataFrame representing vertices and their attributes. + The index uses vertex IDs, from 0 to N - 1 where N is the number of + vertices. + + If you want to use vertex names as index, you can do: + + >>> df = graph.get_vertex_dataframe() + >>> df.set_index('name', inplace=True) + + """ + try: + import pandas as pd + except ImportError: + raise ImportError("You should install pandas in order to use this function") + + df = pd.DataFrame( + {attr: self.vs[attr] for attr in self.vertex_attributes()}, + index=list(range(self.vcount())), + ) + df.index.name = "vertex ID" + + return df + + def get_edge_dataframe(self): + """Export edges with attributes to pandas.DataFrame + + @return: a pandas.DataFrame representing edges and their attributes. + The index uses edge IDs, from 0 to M - 1 where M is the number of + edges. The first two columns of the dataframe represent the IDs of + source and target vertices for each edge. These columns have names + "source" and "target". If your edges have attributes with the same + names, they will be present in the dataframe, but not in the first + two columns. + + If you want to use source and target vertex IDs as index, you can do: + + >>> df = graph.get_edge_dataframe() + >>> df.set_index(['source', 'target'], inplace=True) + + The index will be a pandas.MultiIndex. You can use the `drop=False` + option to keep the `source` and `target` columns. + + If you want to use vertex names in the source and target columns: + + >>> df = graph.get_edge_dataframe() + >>> df_vert = graph.get_vertex_dataframe() + >>> df['source'].replace(df_vert['name'], inplace=True) + >>> df['target'].replace(df_vert['name'], inplace=True) + >>> df_vert.set_index('name', inplace=True) # Optional + + """ + try: + import pandas as pd + except ImportError: + raise ImportError("You should install pandas in order to use this function") + + df = pd.DataFrame( + {attr: self.es[attr] for attr in self.edge_attributes()}, + index=list(range(self.ecount())), + ) + df.index.name = "edge ID" + + df.insert(0, "source", [e.source for e in self.es], allow_duplicates=True) + df.insert(1, "target", [e.target for e in self.es], allow_duplicates=True) + + return df + def bipartite_projection( self, types="type", multiplicity=True, probe1=-1, which="both" ): diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 8f2ace5d0..ec754f50d 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -335,6 +335,84 @@ def testPickle(self): self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) + @skipIf(pd is None, "test case depends on Pandas") + def testVertexDataFrames(self): + g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + + # No vertex names, no attributes + df = g.get_vertex_dataframe() + self.assertEqual(df.shape, (5, 0)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + + # Vertex names, no attributes + g.vs["name"] = ["eggs", "spam", "ham", "bacon", "yello"] + df = g.get_vertex_dataframe() + self.assertEqual(df.shape, (5, 1)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual(list(df["name"]), g.vs["name"]) + self.assertEqual(list(df.columns), ["name"]) + + # Vertex names and attributes (common case) + g.vs["weight"] = [0, 5, 1, 4, 42] + df = g.get_vertex_dataframe() + self.assertEqual(df.shape, (5, 2)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual(list(df["name"]), g.vs["name"]) + self.assertEqual(set(df.columns), set(["name", "weight"])) + self.assertEqual(list(df["weight"]), g.vs["weight"]) + + # No vertex names, with attributes (common case) + g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + g.vs["weight"] = [0, 5, 1, 4, 42] + df = g.get_vertex_dataframe() + self.assertEqual(df.shape, (5, 1)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual(list(df.columns), ["weight"]) + self.assertEqual(list(df["weight"]), g.vs["weight"]) + + @skipIf(pd is None, "test case depends on Pandas") + def testEdgeDataFrames(self): + g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + + # No edge names, no attributes + df = g.get_edge_dataframe() + self.assertEqual(df.shape, (5, 2)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual(list(df.columns), ["source", "target"]) + + # Edge names, no attributes + g.es["name"] = ["my", "list", "of", "five", "edges"] + df = g.get_edge_dataframe() + self.assertEqual(df.shape, (5, 3)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual(list(df["name"]), g.es["name"]) + self.assertEqual(set(df.columns), set(["source", "target", "name"])) + + # No edge names, with attributes + g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) + g.es["weight"] = [6, -0.4, 0, 1, 3] + df = g.get_edge_dataframe() + self.assertEqual(df.shape, (5, 3)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual(set(df.columns), set(["source", "target", "weight"])) + self.assertEqual(list(df["weight"]), g.es["weight"]) + + # Edge names, with weird attributes + g.es["name"] = ["my", "list", "of", "five", "edges"] + g.es["weight"] = [6, -0.4, 0, 1, 3] + g.es["source"] = ["this", "is", "a", "little", "tricky"] + df = g.get_edge_dataframe() + self.assertEqual(df.shape, (5, 5)) + self.assertEqual(list(df.index), [0, 1, 2, 3, 4]) + self.assertEqual( + set(df.columns), set(["source", "target", "name", "source", "weight"]) + ) + self.assertEqual(list(df["name"]), g.es["name"]) + self.assertEqual(list(df["weight"]), g.es["weight"]) + + i = 2 + list(df.columns[2:]).index("source") + self.assertEqual(list(df.iloc[:, i]), g.es["source"]) + def suite(): foreign_suite = unittest.makeSuite(ForeignTests) From 90e8910f847087bfed577adddb522cd434656ada Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 21 Jan 2021 09:57:27 +1100 Subject: [PATCH 0095/1681] Tests for new index-only API --- appveyor.yml | 3 +++ tests/test_foreign.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 27bad4eb4..dca9b2d48 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,6 +38,9 @@ install: # install cibuildwheel - pip install cibuildwheel==1.6.1 + # install pandas (optional) + - pip install pandas + before_build: - git submodule update --init --recursive diff --git a/tests/test_foreign.py b/tests/test_foreign.py index ec754f50d..fc3d2ec69 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -4,7 +4,12 @@ from igraph import * -from .utils import temporary_file +from .utils import temporary_file, skipIf + +try: + import pandas as pd +except ImportError: + pd = None class ForeignTests(unittest.TestCase): From e1683ec2cd3053d02248d066a3994aa7ed40dcc9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 25 Jan 2021 13:12:16 +0100 Subject: [PATCH 0096/1681] fix: bringing codebase up-to-date with the develop branch of the C core --- setup.py | 2 +- src/_igraph/convert.c | 1 - src/_igraph/graphobject.c | 71 ++++++++++++++++++++++++--------------- tests/test_attributes.py | 14 +++----- vendor/source/igraph | 1 - 5 files changed, 49 insertions(+), 40 deletions(-) delete mode 160000 vendor/source/igraph diff --git a/setup.py b/setup.py index df8bbbe35..fd9b18fcf 100644 --- a/setup.py +++ b/setup.py @@ -808,7 +808,7 @@ def use_vendored_igraph(self): the include and library paths and the library names accordingly.""" building_on_windows = building_on_windows_msvc() - buildcfg.include_dirs = [os.path.join("vendor", "install", "igraph", "include")] + buildcfg.include_dirs = [os.path.join("vendor", "install", "igraph", "include", "igraph")] buildcfg.library_dirs = [os.path.join("vendor", "install", "igraph", "lib")] if not buildcfg.static_extension: buildcfg.static_extension = "only_igraph" diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 1dcb2ec1f..4b050b550 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -3047,7 +3047,6 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, static igraphmodule_enum_translation_table_entry_t pagerank_algo_tt[] = { {"prpack", IGRAPH_PAGERANK_ALGO_PRPACK}, {"arpack", IGRAPH_PAGERANK_ALGO_ARPACK}, - {"power", IGRAPH_PAGERANK_ALGO_POWER}, {0,0} }; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 535c324b7..51fee242b 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1563,7 +1563,7 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, igraph_vector_destroy(weights); free(weights); return PyFloat_FromDouble((double)i); } else { - igraph_integer_t i; + igraph_real_t i; if (igraph_diameter(&self->g, &i, 0, 0, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); @@ -1628,8 +1628,8 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, PyObject *dir = Py_True, *vcount_if_unconnected = Py_True; PyObject *weights_o = Py_None; igraph_vector_t *weights = 0; - igraph_integer_t from, to, len; - igraph_real_t len_real; + igraph_integer_t from, to; + igraph_real_t len; static char *kwlist[] = { "directed", "unconn", "weights", NULL }; @@ -1642,7 +1642,7 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, ATTRIBUTE_TYPE_EDGE)) return NULL; if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &len_real, &from, &to, 0, + if (igraph_diameter_dijkstra(&self->g, weights, &len, &from, &to, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(weights); free(weights); @@ -1650,8 +1650,8 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, } igraph_vector_destroy(weights); free(weights); if (from >= 0) - return Py_BuildValue("lld", (long)from, (long)to, (double)len_real); - return Py_BuildValue("OOd", Py_None, Py_None, (double)len_real); + return Py_BuildValue("lld", (long)from, (long)to, (double)len); + return Py_BuildValue("OOd", Py_None, Py_None, (double)len); } else { if (igraph_diameter(&self->g, &len, &from, &to, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { @@ -3657,7 +3657,7 @@ PyObject *igraphmodule_Graph_average_path_length(igraphmodule_GraphObject * &PyBool_Type, &unconn)) return NULL; - if (igraph_average_path_length(&self->g, &res, (directed == Py_True), + if (igraph_average_path_length(&self->g, &res, 0, (directed == Py_True), (unconn == Py_True))) { igraphmodule_handle_igraph_error(); return NULL; @@ -5092,7 +5092,6 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel PyObject *algo_o = Py_None; long niter=1000; float eps=0.001f; - igraph_pagerank_power_options_t popts; void *opts; int retval; @@ -5150,11 +5149,7 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel if (igraphmodule_PyObject_to_pagerank_algo_t(algo_o, &algo)) return NULL; - popts.niter = (igraph_integer_t) niter; popts.eps = eps; - - if (algo == IGRAPH_PAGERANK_ALGO_POWER) { - opts = &popts; - } else if (algo == IGRAPH_PAGERANK_ALGO_ARPACK) { + if (algo == IGRAPH_PAGERANK_ALGO_ARPACK) { opts = igraphmodule_ARPACKOptions_get(arpack_options); } else { opts = 0; @@ -11030,13 +11025,15 @@ PyObject *igraphmodule_Graph_coreness(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = {"membership", "weights", 0}; + static char *kwlist[] = {"membership", "weights", "resolution", "directed", 0}; igraph_vector_t membership; igraph_vector_t *weights=0; + double resolution; igraph_real_t modularity; PyObject *mvec, *wvec=Py_None; + PyObject *directed = Py_True; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &mvec, &wvec)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OdO", kwlist, &mvec, &wvec, &resolution, &directed)) return NULL; if (igraphmodule_PyObject_to_vector_t(mvec, &membership, 1)) @@ -11046,13 +11043,20 @@ PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, igraph_vector_destroy(&membership); return NULL; } - if (igraph_modularity(&self->g, &membership, &modularity, weights)) { + + if (igraph_modularity(&self->g, &membership, weights, resolution, PyObject_IsTrue(directed), &modularity)) { igraph_vector_destroy(&membership); - if (weights) { igraph_vector_destroy(weights); free(weights); } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } return NULL; } + igraph_vector_destroy(&membership); - if (weights) { igraph_vector_destroy(weights); free(weights); } + if (weights) { + igraph_vector_destroy(weights); free(weights); + } + return Py_BuildValue("d", (double)modularity); } @@ -11397,14 +11401,15 @@ PyObject *igraphmodule_Graph_community_label_propagation( PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { - static char *kwlist[] = { "weights", "return_levels", NULL }; + static char *kwlist[] = { "weights", "return_levels", "resolution", NULL }; PyObject *return_levels = Py_False; PyObject *mss, *qs, *res, *weights = Py_None; igraph_matrix_t memberships; igraph_vector_t membership, modularity; + double resolution; igraph_vector_t *ws; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &weights, &return_levels)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOd", kwlist, &weights, &return_levels, &resolution)) { return NULL; } @@ -11415,7 +11420,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self igraph_vector_init(&membership, 0); igraph_vector_init(&modularity, 0); - if (igraph_community_multilevel(&self->g, ws, &membership, &memberships, + if (igraph_community_multilevel(&self->g, ws, resolution, &membership, &memberships, &modularity)) { if (ws) { igraph_vector_destroy(ws); free(ws); } igraph_vector_destroy(&membership); @@ -15474,16 +15479,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*********************************/ {"modularity", (PyCFunction) igraphmodule_Graph_modularity, METH_VARARGS | METH_KEYWORDS, - "modularity(membership, weights=None)\n\n" + "modularity(membership, weights=None, resolution=1, directed=True)\n\n" "Calculates the modularity of the graph with respect to some vertex types.\n\n" "The modularity of a graph w.r.t. some division measures how good the\n" "division is, or how separated are the different vertex types from each\n" - "other. It is defined as M{Q=1/(2m) * sum(Aij-ki*kj/(2m)delta(ci,cj),i,j)}.\n" + "other. It is defined as M{Q=1/(2m) * sum(Aij-gamma*ki*kj/(2m)delta(ci,cj),i,j)}.\n" "M{m} is the number of edges, M{Aij} is the element of the M{A} adjacency\n" "matrix in row M{i} and column M{j}, M{ki} is the degree of node M{i},\n" - "M{kj} is the degree of node M{j}, and M{Ci} and C{cj} are the types of\n" - "the two vertices (M{i} and M{j}). M{delta(x,y)} is one iff M{x=y}, 0\n" - "otherwise.\n\n" + "M{kj} is the degree of node M{j}, M{Ci} and C{cj} are the types of\n" + "the two vertices (M{i} and M{j}), and M{gamma} is a resolution parameter\n" + "that defaults to 1 for the classical definition of modularity. M{delta(x,y)}\n" + "is one iff M{x=y}, 0 otherwise.\n\n" "If edge weights are given, the definition of modularity is modified as\n" "follows: M{Aij} becomes the weight of the corresponding edge, M{ki}\n" "is the total weight of edges incident on vertex M{i}, M{kj} is the\n" @@ -15496,6 +15502,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " each vertex.\n" "@param weights: optional edge weights or C{None} if all edges are weighed\n" " equally.\n" + "@param resolution: the resolution parameter I{gamma} in the formula above.\n" + " The classical definition of modularity is retrieved when the resolution\n" + " parameter is set to 1.\n" + "@param directed: whether to consider edge directions if the graph is directed.\n" + " C{True} will use the directed variant of the modularity measure where the\n" + " in- and out-degrees of nodes are treated separately; C{False} will treat\n" + " directed graphs as undirected.\n" "@return: the modularity score. Score larger than 0.3 usually indicates\n" " strong community structure.\n" "@newfield ref: Reference\n" @@ -15623,7 +15636,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_multilevel", (PyCFunction) igraphmodule_Graph_community_multilevel, METH_VARARGS | METH_KEYWORDS, - "community_multilevel(weights=None, return_levels=True)\n\n" + "community_multilevel(weights=None, return_levels=True, resolution=1)\n\n" "Finds the community structure of the graph according to the multilevel\n" "algorithm of Blondel et al. This is a bottom-up algorithm: initially\n" "every vertex belongs to a separate community, and vertices are moved\n" @@ -15641,6 +15654,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param return_levels: if C{True}, returns the multilevel result. If\n" " C{False}, only the best level (corresponding to the best modularity)\n" " is returned.\n" + "@param resolution: the resolution parameter to use in the modularity measure.\n" + " Smaller values result in a smaller number of larger clusters, while higher\n" + " values yield a large number of small clusters. The classical modularity\n" + " measure assumes a resolution parameter of 1.\n" "@return: either a single list describing the community membership of each\n" " vertex (if C{return_levels} is C{False}), or a list of community membership\n" " vectors, one corresponding to each level and a list of corresponding\n" diff --git a/tests/test_attributes.py b/tests/test_attributes.py index a95c49261..4e2522f56 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -1,7 +1,8 @@ # vim:ts=4 sw=4 sts=4: import sys import unittest -from igraph import * + +from igraph import Graph class AttributeTests(unittest.TestCase): @@ -181,13 +182,6 @@ def testCombinationProd(self): self.assertTrue(g.es["weight"] == [2, 3, 120]) self.assertTrue(g.es["weight2"] == [2, 3, 120]) - def testCombinationMedian(self): - g = self.g - g.es["weight2"] = [1, 0, 2, 4, 8, 6, 7] - g.simplify(combine_edges="median") - self.assertTrue(g.es["weight"] == [1.5, 3, 5]) - self.assertTrue(g.es["weight2"] == [0.5, 2, 6]) - def testCombinationFirst(self): g = self.g g.es["weight2"] = [1, 0, 2, 6, 8, 4, 7] @@ -229,8 +223,8 @@ def testCombinationIgnoreAsNone(self): def testCombinationFunction(self): g = self.g - def join_dash(l): - return "-".join(l) + def join_dash(items): + return "-".join(items) g.es["name"] = list("ABCDEFG") g.simplify(combine_edges={"weight": max, "name": join_dash}) diff --git a/vendor/source/igraph b/vendor/source/igraph deleted file mode 160000 index cafe1e4d4..000000000 --- a/vendor/source/igraph +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cafe1e4d447dded9f640fd4f14b7973bef045d62 From 75e368aa5370b55ec65fdabb8debc2dbfe600dd9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 25 Jan 2021 13:59:17 +0100 Subject: [PATCH 0097/1681] fix: re-adding accidentally deleted submodule --- setup.py | 2 +- vendor/source/igraph | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 160000 vendor/source/igraph diff --git a/setup.py b/setup.py index fd9b18fcf..935196709 100644 --- a/setup.py +++ b/setup.py @@ -645,7 +645,7 @@ def run(self): return custom_sdist def compile_igraph_from_vendor_source(self): - """Compiles igraph from the vendored source code inside `vendor/igraph/source`. + """Compiles igraph from the vendored source code inside `vendor/source/igraph`. This folder typically comes from a git submodule. """ if os.path.exists(os.path.join("vendor", "install", "igraph")): diff --git a/vendor/source/igraph b/vendor/source/igraph new file mode 160000 index 000000000..1d422e68c --- /dev/null +++ b/vendor/source/igraph @@ -0,0 +1 @@ +Subproject commit 1d422e68c735888fff755e23912cc9dac269ce40 From 4dda6fd21c32a71f401c73ffcc6790775e9ee83b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 25 Jan 2021 16:31:18 +0100 Subject: [PATCH 0098/1681] fix: updated the interface of a few more functions according to the develop branch of igraph --- setup.py | 7 +++++-- src/_igraph/graphobject.c | 23 +++++++++-------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/setup.py b/setup.py index 935196709..d7177812f 100644 --- a/setup.py +++ b/setup.py @@ -476,10 +476,13 @@ def compile_in(self, source_folder, build_folder, install_folder): if retcode: return False + return [] + def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries ): - raise NotImplementedError + # Nothing to do; we already installed stuff in the compilation step + return True ########################################################################### @@ -690,7 +693,7 @@ def compile_igraph_from_vendor_source(self): finally: os.chdir(cwd) - if not libraries or not igraph_builder.copy_build_artifacts( + if not igraph_builder.copy_build_artifacts( source_folder=source_folder, build_folder=build_folder, install_folder=install_folder, diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 51fee242b..a9674919a 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3685,6 +3685,8 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, igraph_bool_t return_single = 0; igraph_vs_t vs; + /* nobigint is now unused but we kept here for sake of backwards compatibility */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, &vobj, &directed, &cutoff, &weights_o, &nobigint)) { @@ -3707,8 +3709,7 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, } if (cutoff == Py_None) { - if (igraph_betweenness(&self->g, &res, vs, PyObject_IsTrue(directed), - weights, PyObject_IsTrue(nobigint))) { + if (igraph_betweenness(&self->g, &res, vs, PyObject_IsTrue(directed), weights)) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -3724,8 +3725,7 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, return NULL; } if (igraph_betweenness_estimate(&self->g, &res, vs, PyObject_IsTrue(directed), - (igraph_real_t)PyFloat_AsDouble(cutoff_num), weights, - PyObject_IsTrue(nobigint))) { + (igraph_real_t)PyFloat_AsDouble(cutoff_num), weights)) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -7078,6 +7078,9 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, PyObject *arpack_options_o = igraphmodule_arpack_options_default; PyObject *result; + /* arpack_options_o is now unused but we kept here for sake of backwards + * compatibility */ + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlO!", kwlist, &dist_o, &dim, &igraphmodule_ARPACKOptionsType, &arpack_options_o)) @@ -7103,9 +7106,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, return NULL; } - arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_layout_mds(&self->g, &m, dist, dim, - igraphmodule_ARPACKOptions_get(arpack_options))) { + if (igraph_layout_mds(&self->g, &m, dist, dim)) { if (dist) { igraph_matrix_destroy(dist); free(dist); } @@ -12798,7 +12799,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_betweenness[_estimate] */ {"betweenness", (PyCFunction) igraphmodule_Graph_betweenness, METH_VARARGS | METH_KEYWORDS, - "betweenness(vertices=None, directed=True, cutoff=None, weights=None, nobigint=True)\n\n" + "betweenness(vertices=None, directed=True, cutoff=None, weights=None)\n\n" "Calculates or estimates the betweenness of vertices in a graph.\n\n" "Keyword arguments:\n" "@param vertices: the vertices for which the betweennesses must be returned.\n" @@ -12810,12 +12811,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " returned.\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param nobigint: if C{True}, igraph uses the longest available integer\n" - " type on the current platform to count shortest paths. For some large\n" - " networks that have a specific structure, the counters may overflow.\n" - " To prevent this, use C{nobigint=False}, which forces igraph to use\n" - " arbitrary precision integers at the expense of increased computation\n" - " time.\n" "@return: the (possibly estimated) betweenness of the given vertices in a list\n"}, /* interface to biconnected_components */ From 4c7e253d8544fc9535eadd0ca2f213e61798abe6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 25 Jan 2021 18:39:44 +0100 Subject: [PATCH 0099/1681] fix: link to libigraph.a statically in the CMake build --- setup.py | 20 ++++++++++++++++---- src/_igraph/graphobject.c | 1 - 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index d7177812f..addc0a5fd 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,9 @@ ########################################################################### +is_windows = platform.system() == "windows" + + def building_on_windows_msvc(): """Returns True when using the non-MinGW CPython interpreter on Windows""" return platform.system() == "Windows" and sysconfig.get_platform() != "mingw" @@ -232,7 +235,7 @@ def wait_for_keypress(seconds): """Wait for a keypress or until the given number of seconds have passed, whichever happens first. """ - is_windows = platform.system() == "windows" + global is_windows while seconds > 0: if seconds > 1: @@ -410,11 +413,12 @@ def copy_build_artifacts( ): shutil.copy(fname, os.path.join(install_folder, "lib")) + return True + + def create_build_config_file(self, install_folder, libraries): with open(os.path.join(install_folder, "build.cfg"), "w") as f: f.write(repr(libraries)) - return True - @staticmethod def enhanced_env(**kwargs): env = os.environ.copy() @@ -444,6 +448,8 @@ def compile_in(self, source_folder, build_folder, install_folder): files. build_folder is the name of the folder where the build should be executed. Both must be absolute paths. """ + global is_windows + cmake = find_executable("cmake") if not cmake: print( @@ -476,7 +482,7 @@ def compile_in(self, source_folder, build_folder, install_folder): if retcode: return False - return [] + return ["igraph"] def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries @@ -484,6 +490,10 @@ def copy_build_artifacts( # Nothing to do; we already installed stuff in the compilation step return True + def create_build_config_file(self, install_folder, libraries): + with open(os.path.join(install_folder, "build.cfg"), "w") as f: + f.write(repr(libraries)) + ########################################################################### @@ -703,6 +713,8 @@ def compile_igraph_from_vendor_source(self): print("") sys.exit(1) + igraph_builder.create_build_config_file(install_folder, libraries) + self.use_vendored_igraph() return True diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index a9674919a..d091d5e93 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -7073,7 +7073,6 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, igraph_matrix_t m; igraph_matrix_t *dist = 0; long int dim = 2; - igraphmodule_ARPACKOptionsObject *arpack_options; PyObject *dist_o = Py_None; PyObject *arpack_options_o = igraphmodule_arpack_options_default; PyObject *result; From 6c2590ba18b81f22a67e6034a38c4dbcbcbde3d1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 26 Jan 2021 14:46:10 +0100 Subject: [PATCH 0100/1681] updating tests not to use deprecated constructs from igraph 0.8 --- setup.py | 60 +++++++++++++++++------------ src/_igraph/graphobject.c | 35 +++++++++-------- src/_igraph/igraphmodule.c | 77 +++++++++++++++++++++++++++++++++++-- tests/test_basic.py | 69 ++++++++++++++++++++++++++++++++- tests/test_decomposition.py | 29 ++++++++------ tests/test_structural.py | 37 +++++++++--------- 6 files changed, 231 insertions(+), 76 deletions(-) diff --git a/setup.py b/setup.py index addc0a5fd..a522c89b8 100644 --- a/setup.py +++ b/setup.py @@ -272,7 +272,38 @@ def wait_for_keypress(seconds): ########################################################################### -class IgraphCCoreAutotoolsBuilder(object): +class IgraphCCoreBuilder(object): + """Superclass for classes responsible for downloading and building the + C core of igraph if it is not installed yet. + """ + + def create_build_config_file(self, install_folder, libraries): + with open(os.path.join(install_folder, "build.cfg"), "w") as f: + f.write(repr(libraries)) + + def parse_pkgconfig_file(self, filename): + building_on_windows = building_on_windows_msvc() + + if building_on_windows: + libraries = ["igraph"] + else: + libraries = [] + with open(filename, "r") as fp: + for line in fp: + if line.startswith("Libs: ") or line.startswith("Libs.private: "): + words = line.strip().split() + libraries.extend( + word[2:] for word in words if word.startswith("-l") + ) + + if not libraries: + # Educated guess + libraries = ["igraph"] + + return libraries + + +class IgraphCCoreAutotoolsBuilder(IgraphCCoreBuilder): """Class responsible for downloading and building the C core of igraph if it is not installed yet, assuming that the C core uses `configure.ac` and its friends. This used to be the case before igraph 0.9. @@ -362,20 +393,7 @@ def compile_in(self, source_folder, build_folder, install_folder): if retcode: return False - if building_on_windows: - libraries = ["igraph"] - else: - libraries = [] - for line in open("igraph.pc"): - if line.startswith("Libs: ") or line.startswith("Libs.private: "): - words = line.strip().split() - libraries.extend( - word[2:] for word in words if word.startswith("-l") - ) - - if not libraries: - # Educated guess - libraries = ["igraph"] + libraries = self.parse_pkgconfig_file("igraph.pc") return libraries @@ -415,10 +433,6 @@ def copy_build_artifacts( return True - def create_build_config_file(self, install_folder, libraries): - with open(os.path.join(install_folder, "build.cfg"), "w") as f: - f.write(repr(libraries)) - @staticmethod def enhanced_env(**kwargs): env = os.environ.copy() @@ -431,7 +445,7 @@ def enhanced_env(**kwargs): ########################################################################### -class IgraphCCoreCMakeBuilder(object): +class IgraphCCoreCMakeBuilder(IgraphCCoreBuilder): """Class responsible for downloading and building the C core of igraph if it is not installed yet, assuming that the C core uses CMake as the build tool. This is the case from igraph 0.9. @@ -482,7 +496,7 @@ def compile_in(self, source_folder, build_folder, install_folder): if retcode: return False - return ["igraph"] + return self.parse_pkgconfig_file(os.path.join(install_folder, "lib", "pkgconfig", "igraph.pc")) def copy_build_artifacts( self, source_folder, build_folder, install_folder, libraries @@ -490,10 +504,6 @@ def copy_build_artifacts( # Nothing to do; we already installed stuff in the compilation step return True - def create_build_config_file(self, install_folder, libraries): - with open(os.path.join(install_folder, "build.cfg"), "w") as f: - f.write(repr(libraries)) - ########################################################################### diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d091d5e93..195926804 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1539,6 +1539,7 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, PyObject *dir = Py_True, *vcount_if_unconnected = Py_True; PyObject *weights_o = Py_None; igraph_vector_t *weights = 0; + igraph_real_t diameter; static char *kwlist[] = { "directed", "unconn", "weights", NULL @@ -1553,23 +1554,28 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, ATTRIBUTE_TYPE_EDGE)) return NULL; if (weights) { - igraph_real_t i; - if (igraph_diameter_dijkstra(&self->g, weights, &i, 0, 0, 0, + if (igraph_diameter_dijkstra(&self->g, weights, &diameter, 0, 0, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(weights); free(weights); return NULL; } igraph_vector_destroy(weights); free(weights); - return PyFloat_FromDouble((double)i); + return PyFloat_FromDouble((double)diameter); } else { - igraph_real_t i; - if (igraph_diameter(&self->g, &i, 0, 0, 0, PyObject_IsTrue(dir), + if (igraph_diameter(&self->g, &diameter, 0, 0, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); return NULL; } - return PyInt_FromLong((long)i); + + /* The diameter is integer in this case, except if igraph_diameter() + * returned NaN or infinity for some reason */ + if (ceilf(diameter) == diameter && isfinite(diameter)) { + return PyInt_FromLong((long)diameter); + } else { + return PyFloat_FromDouble((double)diameter); + } } } @@ -3724,8 +3730,8 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } - if (igraph_betweenness_estimate(&self->g, &res, vs, PyObject_IsTrue(directed), - (igraph_real_t)PyFloat_AsDouble(cutoff_num), weights)) { + if (igraph_betweenness_cutoff(&self->g, &res, vs, PyObject_IsTrue(directed), + weights, (igraph_real_t)PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4072,9 +4078,8 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); igraph_vector_destroy(&res); return NULL; } - if (igraph_closeness_estimate(&self->g, &res, vs, mode, - (igraph_real_t)PyFloat_AsDouble(cutoff_num), weights, - PyObject_IsTrue(normalized_o))) { + if (igraph_closeness_cutoff(&self->g, &res, vs, mode, weights, + (igraph_real_t)PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4457,8 +4462,8 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_vector_destroy(&res); return NULL; } - if (igraph_edge_betweenness_estimate(&self->g, &res, PyObject_IsTrue(directed), - (igraph_real_t)PyFloat_AsDouble(cutoff_num), weights)) { + if (igraph_edge_betweenness_cutoff(&self->g, &res, PyObject_IsTrue(directed), + weights, (igraph_real_t)PyFloat_AsDouble(cutoff_num))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -11028,7 +11033,7 @@ PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, static char *kwlist[] = {"membership", "weights", "resolution", "directed", 0}; igraph_vector_t membership; igraph_vector_t *weights=0; - double resolution; + double resolution = 1; igraph_real_t modularity; PyObject *mvec, *wvec=Py_None; PyObject *directed = Py_True; @@ -11406,7 +11411,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self PyObject *mss, *qs, *res, *weights = Py_None; igraph_matrix_t memberships; igraph_vector_t membership, modularity; - double resolution; + double resolution = 1; igraph_vector_t *ws; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOd", kwlist, &weights, &return_levels, &resolution)) { diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 5e671486a..8b4da52df 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -494,6 +494,57 @@ PyObject* igraphmodule_is_graphical_degree_sequence(PyObject *self, } +PyObject* igraphmodule_is_graphical(PyObject *self, PyObject *args, PyObject *kwds) { + static char* kwlist[] = { "out_deg", "in_deg", "loops", "multiple", NULL }; + PyObject *out_deg_o = 0, *in_deg_o = 0; + PyObject *loops = Py_False, *multiple = Py_False; + igraph_vector_t out_deg, in_deg; + igraph_bool_t is_directed, result; + int allowed_edge_types; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, + &out_deg_o, &in_deg_o, &loops, &multiple)) + return NULL; + + is_directed = (in_deg_o != 0 && in_deg_o != Py_None); + + if (igraphmodule_PyObject_to_vector_t(out_deg_o, &out_deg, 0)) + return NULL; + + if (is_directed && igraphmodule_PyObject_to_vector_t(in_deg_o, &in_deg, 0)) { + igraph_vector_destroy(&out_deg); + return NULL; + } + + allowed_edge_types = IGRAPH_SIMPLE_SW; + if (PyObject_IsTrue(loops)) { + allowed_edge_types |= IGRAPH_LOOPS_SW; + } + if (PyObject_IsTrue(multiple)) { + allowed_edge_types |= IGRAPH_MULTI_SW; + } + + if (igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, allowed_edge_types, &result)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&out_deg); + if (is_directed) { + igraph_vector_destroy(&in_deg); + } + return NULL; + } + + igraph_vector_destroy(&out_deg); + if (is_directed) { + igraph_vector_destroy(&in_deg); + } + + if (result) + Py_RETURN_TRUE; + else + Py_RETURN_FALSE; +} + + PyObject* igraphmodule_power_law_fit(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "data", "xmin", "force_continuous", NULL }; PyObject *data_o, *force_continuous_o = Py_False; @@ -582,6 +633,7 @@ static PyMethodDef igraphmodule_methods[] = {"is_degree_sequence", (PyCFunction)igraphmodule_is_degree_sequence, METH_VARARGS | METH_KEYWORDS, "is_degree_sequence(out_deg, in_deg=None)\n\n" + "Deprecated since 0.9 in favour of L{is_graphical()}.\n\n" "Returns whether a list of degrees can be a degree sequence of some graph.\n\n" "Note that it is not required for the graph to be simple; in other words,\n" "this function may return C{True} for degree sequences that can be realized\n" @@ -596,13 +648,27 @@ static PyMethodDef igraphmodule_methods[] = "@param in_deg: the list of in-degrees for directed graphs. This parameter\n" " must be C{None} for undirected graphs.\n" "@return: C{True} if there exists some graph that can realize the given degree\n" - " sequence, C{False} otherwise." - "@see: L{is_graphical_degree_sequence()} if you do not want to allow multiple\n" - " or loop edges.\n" + " sequence, C{False} otherwise.\n" + }, + {"is_graphical", (PyCFunction)igraphmodule_is_graphical, + METH_VARARGS | METH_KEYWORDS, + "is_graphical(out_deg, in_deg=None, loops=False, multiple=False)\n\n" + "Returns whether a list of degrees can be a degree sequence of some graph,\n" + "with or without multiple and loop edges, depending on the allowed edge types\n" + "in the remaining arguments.\n\n" + "@param out_deg: the list of degrees. For directed graphs, this list must\n" + " contain the out-degrees of the vertices.\n" + "@param in_deg: the list of in-degrees for directed graphs. This parameter\n" + " must be C{None} for undirected graphs.\n" + "@param loops: whether loop edges are allowed.\n" + "@param multiple: whether multiple edges are allowed.\n" + "@return: C{True} if there exists some graph that can realize the given\n" + " degree sequence with the given edge types, C{False} otherwise.\n" }, {"is_graphical_degree_sequence", (PyCFunction)igraphmodule_is_graphical_degree_sequence, METH_VARARGS | METH_KEYWORDS, "is_graphical_degree_sequence(out_deg, in_deg=None)\n\n" + "Deprecated since 0.9 in favour of L{is_graphical()}.\n\n" "Returns whether a list of degrees can be a degree sequence of some simple graph.\n\n" "Note that it is required for the graph to be simple; in other words,\n" "this function will return C{False} for degree sequences that cannot be realized\n" @@ -613,7 +679,6 @@ static PyMethodDef igraphmodule_methods[] = " must be C{None} for undirected graphs.\n" "@return: C{True} if there exists some simple graph that can realize the given\n" " degree sequence, C{False} otherwise.\n" - "@see: L{is_degree_sequence()} if you want to allow multiple or loop edges.\n" }, {"set_progress_handler", igraphmodule_set_progress_handler, METH_O, "set_progress_handler(handler)\n\n" @@ -857,6 +922,10 @@ extern PyObject* igraphmodule_arpack_options_default; PyModule_AddIntConstant(m, "TRANSITIVITY_NAN", IGRAPH_TRANSITIVITY_NAN); PyModule_AddIntConstant(m, "TRANSITIVITY_ZERO", IGRAPH_TRANSITIVITY_ZERO); + PyModule_AddIntConstant(m, "SIMPLE_SW", IGRAPH_SIMPLE_SW); + PyModule_AddIntConstant(m, "LOOPS_SW", IGRAPH_LOOPS_SW); + PyModule_AddIntConstant(m, "MULTI_SW", IGRAPH_MULTI_SW); + /* More useful constants */ { const char* version; diff --git a/tests/test_basic.py b/tests/test_basic.py index 2d7c7be6a..469506597 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,4 +1,7 @@ import unittest +import warnings + +from functools import partial from igraph import ( ALL, @@ -6,6 +9,7 @@ IN, InternalError, is_degree_sequence, + is_graphical, is_graphical_degree_sequence, Matrix, ) @@ -601,6 +605,65 @@ def checkIfOK(self, g, name_attr, edge_attrs): class DegreeSequenceTests(unittest.TestCase): def testIsDegreeSequence(self): + # Catch and suppress warnings because is_degree_sequence() is now + # deprecated + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertTrue(is_degree_sequence([])) + self.assertTrue(is_degree_sequence([], [])) + self.assertTrue(is_degree_sequence([0])) + self.assertTrue(is_degree_sequence([0], [0])) + self.assertFalse(is_degree_sequence([1])) + self.assertTrue(is_degree_sequence([1], [1])) + self.assertTrue(is_degree_sequence([2])) + self.assertFalse(is_degree_sequence([2, 1, 1, 1])) + self.assertTrue(is_degree_sequence([2, 1, 1, 1], [1, 1, 1, 2])) + self.assertFalse(is_degree_sequence([2, 1, -2])) + self.assertFalse(is_degree_sequence([2, 1, 1, 1], [1, 1, 1, -2])) + self.assertTrue(is_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3])) + self.assertTrue(is_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3], None)) + self.assertFalse(is_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])) + self.assertTrue( + is_degree_sequence( + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [4, 3, 2, 3, 4, 4, 2, 2, 4, 2] + ) + ) + + def testIsGraphicalSequence(self): + # Catch and suppress warnings because is_graphical_degree_sequence() is now + # deprecated + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + self.assertTrue(is_graphical_degree_sequence([])) + self.assertTrue(is_graphical_degree_sequence([], [])) + self.assertTrue(is_graphical_degree_sequence([0])) + self.assertTrue(is_graphical_degree_sequence([0], [0])) + self.assertFalse(is_graphical_degree_sequence([1])) + self.assertFalse(is_graphical_degree_sequence([1], [1])) + self.assertFalse(is_graphical_degree_sequence([2])) + self.assertFalse(is_graphical_degree_sequence([2, 1, 1, 1])) + self.assertTrue(is_graphical_degree_sequence([2, 1, 1, 1], [1, 1, 1, 2])) + self.assertFalse(is_graphical_degree_sequence([2, 1, -2])) + self.assertFalse(is_graphical_degree_sequence([2, 1, 1, 1], [1, 1, 1, -2])) + self.assertTrue( + is_graphical_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3]) + ) + self.assertTrue( + is_graphical_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3], None) + ) + self.assertFalse( + is_graphical_degree_sequence([3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]) + ) + self.assertTrue( + is_graphical_degree_sequence( + [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], [4, 3, 2, 3, 4, 4, 2, 2, 4, 2] + ) + ) + self.assertTrue(is_graphical_degree_sequence([3, 3, 3, 3, 4])) + + def testIsGraphicalNonSimple(self): + # Same as testIsDegreeSequence, but using is_graphical() + is_degree_sequence = partial(is_graphical, loops=True, multiple=True) self.assertTrue(is_degree_sequence([])) self.assertTrue(is_degree_sequence([], [])) self.assertTrue(is_degree_sequence([0])) @@ -621,7 +684,11 @@ def testIsDegreeSequence(self): ) ) - def testIsGraphicalSequence(self): + def testIsGraphicalSimple(self): + # Same as testIsGraphicalDegreeSequence, but using is_graphical() + is_graphical_degree_sequence = partial( + is_graphical, loops=False, multiple=False + ) self.assertTrue(is_graphical_degree_sequence([])) self.assertTrue(is_graphical_degree_sequence([], [])) self.assertTrue(is_graphical_degree_sequence([0])) diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 3eaef38fb..8413af0d6 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -1,15 +1,20 @@ +import math import random import unittest -import math - -from igraph import * - -try: - set, frozenset -except NameError: - import sets - set, frozenset = sets.Set, sets.ImmutableSet +from igraph import ( + Clustering, + CohesiveBlocks, + Cover, + Graph, + Histogram, + InternalError, + UniqueIdGenerator, + VertexClustering, + compare_communities, + split_join_distance, + set_random_number_generator, +) class SubgraphTests(unittest.TestCase): @@ -63,9 +68,9 @@ def testKCores(self): self.assertTrue(g.coreness() == [3, 3, 3, 3, 1, 1, 1, 2, 1, 2, 2]) self.assertTrue(g.shell_index() == g.coreness()) - l = g.k_core(3).get_edgelist() - l.sort() - self.assertTrue(l == [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]) + edgelist = g.k_core(3).get_edgelist() + edgelist.sort() + self.assertTrue(edgelist == [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]) class ClusteringTests(unittest.TestCase): diff --git a/tests/test_structural.py b/tests/test_structural.py index aadf18033..5fecf4f43 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -2,8 +2,8 @@ import unittest import warnings -from igraph import * -from math import isnan +from igraph import Graph, InternalError, IN, OUT, ALL, TREE_IN +from math import inf, isnan class SimplePropertiesTests(unittest.TestCase): @@ -26,8 +26,8 @@ def testDensity(self): def testDiameter(self): self.assertTrue(self.gfull.diameter() == 1) - self.assertTrue(self.gempty.diameter(unconn=False) == 10) - self.assertTrue(self.gempty.diameter(unconn=False, weights=[]) == float("inf")) + self.assertTrue(isnan(self.gempty.diameter(unconn=False))) + self.assertTrue(isnan(self.gempty.diameter(unconn=False, weights=[]))) self.assertTrue(self.g.diameter() == 2) self.assertTrue(self.gdir.diameter(False) == 2) self.assertTrue(self.gdir.diameter() == 3) @@ -78,11 +78,11 @@ def testLocalTransitivity(self): == [0.0] * self.tree.vcount() ) - l = self.g.transitivity_local_undirected(mode="zero") - self.assertAlmostEqual(2 / 3, l[0], places=4) - self.assertAlmostEqual(2 / 3, l[1], places=4) - self.assertEqual(1, l[2]) - self.assertEqual(1, l[3]) + transitivity = self.g.transitivity_local_undirected(mode="zero") + self.assertAlmostEqual(2 / 3, transitivity[0], places=4) + self.assertAlmostEqual(2 / 3, transitivity[1], places=4) + self.assertEqual(1, transitivity[2]) + self.assertEqual(1, transitivity[3]) g = Graph.Full(4) + 1 + [(0, 4)] g.es["weight"] = [1, 1, 1, 1, 1, 1, 5] @@ -269,7 +269,7 @@ def testClosenessCentrality(self): g = Graph.Star(5) with warnings.catch_warnings(): warnings.simplefilter("ignore") - cl = g.closeness(cutoff=1) + cl = g.closeness(cutoff=0.9) cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -285,7 +285,7 @@ def testClosenessCentrality(self): g = Graph.Star(5) with warnings.catch_warnings(): warnings.simplefilter("ignore") - cl = g.closeness(cutoff=1, weights=weights) + cl = g.closeness(cutoff=0.9, weights=weights) cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -389,17 +389,17 @@ def testAuthorityScore(self): g = Graph.Tree(15, 2, TREE_IN) asc = g.authority_score() self.assertAlmostEqual(max(asc), 1.0, places=3) - asc, ev = g.hub_score(scale=False, return_eigenvalue=True) - if asc[0] < 0: - hs = [-x for x in asc] + + # Smoke testing + g.authority_score(scale=False, return_eigenvalue=True) def testHubScore(self): g = Graph.Tree(15, 2, TREE_IN) hsc = g.hub_score() self.assertAlmostEqual(max(hsc), 1.0, places=3) - hsc, ev = g.hub_score(scale=False, return_eigenvalue=True) - if hsc[0] < 0: - hsc = [-x for x in hsc] + + # Smoke testing + g.hub_score(scale=False, return_eigenvalue=True) def testCoreness(self): g = Graph.Full(4) + Graph(4) + [(0, 4), (1, 5), (2, 6), (3, 7)] @@ -540,7 +540,6 @@ def testShortestPaths(self): ) ws = [0, 2, 1, 0, 5, 2, 1, 1, 0, 2, 2, 8, 1, 1, 3, 1, 1, 4, 2, 1] g.es["weight"] = ws - inf = float("inf") expected = [ [0, 0, 0, 1, 5, 2, 1, 13, 3, 5], [inf, 0, 0, 1, 5, 2, 1, 13, 3, 5], @@ -673,7 +672,7 @@ def testPathLengthHist(self): h = g.path_length_hist() self.assertTrue(h.unconnected == 0) self.assertTrue( - [(int(l), x) for l, _, x in h.bins()] + [(int(value), x) for value, _, x in h.bins()] == [(1, 14), (2, 19), (3, 20), (4, 20), (5, 16), (6, 16)] ) g = Graph.Full(5) + Graph.Full(4) From ddfded23b96ca9c94efd64961ca8bebacd60e27e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 26 Jan 2021 16:26:40 +0100 Subject: [PATCH 0101/1681] almost all test cases fixed now for 0.9 --- src/_igraph/graphobject.c | 613 +++++++++++++++++++------------------- tests/test_structural.py | 8 +- 2 files changed, 318 insertions(+), 303 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 195926804..73ef24061 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -454,11 +454,11 @@ PyObject *igraphmodule_Graph_is_matching(igraphmodule_GraphObject* self, if (igraphmodule_attrib_to_vector_long_t(matching_o, self, &matching, ATTRIBUTE_TYPE_VERTEX)) - return NULL; + return NULL; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) { if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } - return NULL; + return NULL; } if (igraph_is_matching(&self->g, types, matching, &result)) { @@ -496,11 +496,11 @@ PyObject *igraphmodule_Graph_is_maximal_matching(igraphmodule_GraphObject* self, if (igraphmodule_attrib_to_vector_long_t(matching_o, self, &matching, ATTRIBUTE_TYPE_VERTEX)) - return NULL; + return NULL; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) { if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } - return NULL; + return NULL; } if (igraph_is_maximal_matching(&self->g, types, matching, &result)) { @@ -768,7 +768,7 @@ PyObject *igraphmodule_Graph_diversity(igraphmodule_GraphObject * self, } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); igraph_vector_destroy(&result); return NULL; @@ -858,7 +858,7 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); igraph_vector_destroy(&result); return NULL; @@ -1551,7 +1551,7 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; if (weights) { if (igraph_diameter_dijkstra(&self->g, weights, &diameter, 0, 0, 0, @@ -1599,7 +1599,7 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; igraph_vector_init(&res, 0); if (weights) { @@ -1645,7 +1645,7 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; if (weights) { if (igraph_diameter_dijkstra(&self->g, weights, &len, &from, &to, 0, @@ -1655,9 +1655,11 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, return NULL; } igraph_vector_destroy(weights); free(weights); - if (from >= 0) + if (from >= 0) { return Py_BuildValue("lld", (long)from, (long)to, (double)len); - return Py_BuildValue("OOd", Py_None, Py_None, (double)len); + } else { + return Py_BuildValue("OOd", Py_None, Py_None, (double)len); + } } else { if (igraph_diameter(&self->g, &len, &from, &to, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { @@ -1665,9 +1667,22 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, return NULL; } - if (from >= 0) - return Py_BuildValue("lll", (long)from, (long)to, (long)len); - return Py_BuildValue("OOl", Py_None, Py_None, (long)len); + /* if len is finite and integer (which it typically is, unless it's + * infinite), then return a Python int as the third value; otherwise + * return a float */ + if (ceilf(len) == len && isfinite(len)) { + if (from >= 0) { + return Py_BuildValue("lll", (long)from, (long)to, (long)len); + } else { + return Py_BuildValue("OOl", Py_None, Py_None, (long)len); + } + } else { + if (from >= 0) { + return Py_BuildValue("lld", (long)from, (long)to, (double)len); + } else { + return Py_BuildValue("OOd", Py_None, Py_None, (double)len); + } + } } } @@ -1708,7 +1723,7 @@ PyObject *igraphmodule_Graph_girth(igraphmodule_GraphObject *self, */ PyObject *igraphmodule_Graph_convergence_degree(igraphmodule_GraphObject *self) { igraph_vector_t result; - PyObject *o; + PyObject *o; igraph_vector_init(&result, 0); if (igraph_convergence_degree(&self->g, &result, 0, 0)) { @@ -1733,10 +1748,10 @@ PyObject *igraphmodule_Graph_convergence_field_size(igraphmodule_GraphObject *se igraph_vector_init(&ins, 0); igraph_vector_init(&outs, 0); if (igraph_convergence_degree(&self->g, 0, &ins, &outs)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&ins); - igraph_vector_destroy(&outs); - return NULL; + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&ins); + igraph_vector_destroy(&outs); + return NULL; } o1=igraphmodule_vector_t_to_PyList(&ins, IGRAPHMODULE_TYPE_INT); @@ -1783,7 +1798,7 @@ PyObject *igraphmodule_Graph_knn(igraphmodule_GraphObject *self, } if (igraphmodule_attrib_to_vector_t(weights_obj, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vids); igraph_vector_destroy(&knn); igraph_vector_destroy(&knnk); @@ -2127,7 +2142,7 @@ PyObject *igraphmodule_Graph_Degree_Sequence(PyTypeObject * type, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O!O", kwlist, &PyList_Type, &outdeg, &PyList_Type, &indeg, - &method)) + &method)) return NULL; if (igraphmodule_PyObject_to_degseq_t(method, &meth)) return NULL; @@ -2323,7 +2338,7 @@ PyObject *igraphmodule_Graph_Forest_Fire(PyTypeObject * type, if (igraph_forest_fire_game(&g, (igraph_integer_t)n, (igraph_real_t)fw_prob, (igraph_real_t)bw_factor, (igraph_integer_t)ambs, - (igraph_bool_t)(PyObject_IsTrue(directed)))) { + (igraph_bool_t)(PyObject_IsTrue(directed)))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2359,7 +2374,7 @@ PyObject *igraphmodule_Graph_Full(PyTypeObject * type, } if (igraph_full(&g, (igraph_integer_t) n, PyObject_IsTrue(directed), - PyObject_IsTrue(loops))) { + PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2395,19 +2410,19 @@ PyObject *igraphmodule_Graph_Full_Bipartite(PyTypeObject * type, } if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; + return NULL; if (igraph_vector_bool_init(&vertex_types, n1+n2)) { igraphmodule_handle_igraph_error(); - return NULL; + return NULL; } if (igraph_full_bipartite(&g, &vertex_types, (igraph_integer_t) n1, (igraph_integer_t) n2, PyObject_IsTrue(directed), mode)) { - igraph_vector_bool_destroy(&vertex_types); + igraph_vector_bool_destroy(&vertex_types); igraphmodule_handle_igraph_error(); - return NULL; + return NULL; } CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2567,29 +2582,29 @@ PyObject *igraphmodule_Graph_Incidence(PyTypeObject * type, static char *kwlist[] = { "matrix", "directed", "mode", "multiple", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOO", kwlist, &PyList_Type, &matrix_o, - &directed, &mode_o, &multiple)) + &directed, &mode_o, &multiple)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; if (igraph_vector_bool_init(&vertex_types, 0)) { igraphmodule_handle_igraph_error(); - return NULL; + return NULL; } if (igraphmodule_PyList_to_matrix_t(matrix_o, &matrix)) { - igraph_vector_bool_destroy(&vertex_types); + igraph_vector_bool_destroy(&vertex_types); PyErr_SetString(PyExc_TypeError, "Error while converting incidence matrix"); return NULL; } if (igraph_incidence(&g, &vertex_types, &matrix, - PyObject_IsTrue(directed), mode, PyObject_IsTrue(multiple))) { - igraphmodule_handle_igraph_error(); - igraph_matrix_destroy(&matrix); - igraph_vector_bool_destroy(&vertex_types); - return NULL; + PyObject_IsTrue(directed), mode, PyObject_IsTrue(multiple))) { + igraphmodule_handle_igraph_error(); + igraph_matrix_destroy(&matrix); + igraph_vector_bool_destroy(&vertex_types); + return NULL; } igraph_matrix_destroy(&matrix); @@ -3012,15 +3027,15 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, return NULL; if (igraph_vector_bool_init(&vertex_types, n1+n2)) { - igraphmodule_handle_igraph_error(); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; } if (igraph_bipartite_game(&g, &vertex_types, t, - (igraph_integer_t) n1, (igraph_integer_t) n2, - (igraph_real_t) p, (igraph_integer_t) m, - PyObject_IsTrue(directed_o), neimode)) { - igraph_vector_bool_destroy(&vertex_types); + (igraph_integer_t) n1, (igraph_integer_t) n2, + (igraph_real_t) p, (igraph_integer_t) m, + PyObject_IsTrue(directed_o), neimode)) { + igraph_vector_bool_destroy(&vertex_types); igraphmodule_handle_igraph_error(); return NULL; } @@ -3030,7 +3045,7 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, vertex_types_o = igraphmodule_vector_bool_t_to_PyList(&vertex_types); igraph_vector_bool_destroy(&vertex_types); if (vertex_types_o == 0) - return NULL; + return NULL; return Py_BuildValue("NN", (PyObject *) self, vertex_types_o); } @@ -3150,7 +3165,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, igraph_vector_int_t block_sizes; static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", - "loops", NULL }; + "loops", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "lO!O!|OO", kwlist, &n, &PyList_Type, &pref_matrix_o, @@ -3487,8 +3502,8 @@ PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject *self) igraph_vector_t res; PyObject *o; if (igraph_vector_init(&res, 0)) { - igraphmodule_handle_igraph_error(); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; } if (igraph_articulation_points(&self->g, &res)) { @@ -3608,14 +3623,14 @@ PyObject *igraphmodule_Graph_authority_score( igraph_vector_t res, *weights = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, - &scale_o, &igraphmodule_ARPACKOptionsType, + &scale_o, &igraphmodule_ARPACKOptionsType, &arpack_options_o, &return_eigenvalue)) return NULL; if (igraph_vector_init(&res, 0)) return igraphmodule_handle_igraph_error(); if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; if (igraph_authority_score(&self->g, &res, &value, PyObject_IsTrue(scale_o), @@ -3700,7 +3715,7 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; if (igraphmodule_PyObject_to_vs_t(vobj, &vs, &self->g, &return_single, 0)) { if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -3809,7 +3824,7 @@ PyObject *igraphmodule_Graph_bibcoupling(igraphmodule_GraphObject * self, * \sa igraph_biconnected_components */ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *self, - PyObject *args, PyObject *kwds) { + PyObject *args, PyObject *kwds) { igraph_vector_ptr_t components; igraph_vector_t points; igraph_bool_t return_articulation_points; @@ -3822,20 +3837,20 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se return_articulation_points = PyObject_IsTrue(aps); if (igraph_vector_ptr_init(&components, 0)) { - igraphmodule_handle_igraph_error(); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; } if (return_articulation_points) { - if (igraph_vector_init(&points, 0)) { - igraphmodule_handle_igraph_error(); - igraph_vector_ptr_destroy(&components); - return NULL; - } + if (igraph_vector_init(&points, 0)) { + igraphmodule_handle_igraph_error(); + igraph_vector_ptr_destroy(&components); + return NULL; + } } if (igraph_biconnected_components(&self->g, &no, &components, 0, 0, return_articulation_points ? &points : 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_ptr_destroy(&components); + igraph_vector_ptr_destroy(&components); if (return_articulation_points) igraph_vector_destroy(&points); return NULL; } @@ -3845,11 +3860,11 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se igraph_vector_ptr_destroy_all(&components); if (return_articulation_points) { - PyObject *result2; - igraph_vector_sort(&points); - result2 = igraphmodule_vector_t_to_PyList(&points, IGRAPHMODULE_TYPE_INT); + PyObject *result2; + igraph_vector_sort(&points); + result2 = igraphmodule_vector_t_to_PyList(&points, IGRAPHMODULE_TYPE_INT); igraph_vector_destroy(&points); - return Py_BuildValue("NN", result, result2); /* references stolen */ + return Py_BuildValue("NN", result, result2); /* references stolen */ } return result; @@ -3861,7 +3876,7 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se * \sa igraph_bipartite_projection */ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * self, - PyObject* args, PyObject* kwds) { + PyObject* args, PyObject* kwds) { PyObject *types_o = Py_None, *multiplicity_o = Py_True, *mul1 = 0, *mul2 = 0; igraphmodule_GraphObject *result1 = 0, *result2 = 0; igraph_vector_bool_t* types = 0; @@ -3878,7 +3893,7 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel return NULL; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) - return NULL; + return NULL; if (which == 0) { p_g2 = 0; @@ -3973,7 +3988,7 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel * \sa igraph_bipartite_projection_size */ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject * self, - PyObject* args, PyObject* kwds) { + PyObject* args, PyObject* kwds) { PyObject *types_o = Py_None; igraph_vector_bool_t* types = 0; igraph_integer_t vcount1, vcount2, ecount1, ecount2; @@ -3984,10 +3999,10 @@ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject return NULL; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) - return NULL; + return NULL; if (igraph_bipartite_projection_size(&self->g, types, - &vcount1, &ecount1, &vcount2, &ecount2)) { + &vcount1, &ecount1, &vcount2, &ecount2)) { if (types) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; @@ -4007,8 +4022,8 @@ PyObject *igraphmodule_Graph_bridges(igraphmodule_GraphObject *self) { igraph_vector_t res; PyObject *o; if (igraph_vector_init(&res, 0)) { - igraphmodule_handle_igraph_error(); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; } if (igraph_bridges(&self->g, &res)) { @@ -4033,7 +4048,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "vertices", "mode", "cutoff", "weights", - "normalized", NULL }; + "normalized", NULL }; PyObject *vobj = Py_None, *list = NULL, *cutoff = Py_None, *mode_o = Py_None, *weights_o = Py_None, *normalized_o = Py_True; igraph_vector_t res, *weights = 0; @@ -4057,7 +4072,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); return NULL; @@ -4065,7 +4080,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, if (cutoff == Py_None) { if (igraph_closeness(&self->g, &res, vs, mode, weights, - PyObject_IsTrue(normalized_o))) { + PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4244,19 +4259,19 @@ PyObject *igraphmodule_Graph_cocitation(igraphmodule_GraphObject * self, * \sa igraph_contract_vertices */ PyObject *igraphmodule_Graph_contract_vertices(igraphmodule_GraphObject * self, - PyObject * args, PyObject * kwds) { + PyObject * args, PyObject * kwds) { static char* kwlist[] = {"mapping", "combine_attrs", NULL }; PyObject *mapping_o, *combination_o = Py_None; igraph_vector_t mapping; igraph_attribute_combination_t combination; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &mapping_o, - &combination_o)) - return NULL; + &combination_o)) + return NULL; if (igraphmodule_PyObject_to_attribute_combination_t( - combination_o, &combination)) - return NULL; + combination_o, &combination)) + return NULL; if (igraphmodule_PyObject_to_vector_t(mapping_o, &mapping, 1)) { igraph_attribute_combination_destroy(&combination); @@ -4999,7 +5014,7 @@ PyObject *igraphmodule_Graph_neighborhood(igraphmodule_GraphObject *self, } if (igraph_neighborhood(&self->g, &res, vs, (igraph_integer_t) order, mode, - mindist)) { + mindist)) { igraph_vs_destroy(&vs); return igraphmodule_handle_igraph_error(); } @@ -5054,7 +5069,7 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, } if (igraph_neighborhood_size(&self->g, &res, vs, (igraph_integer_t) order, mode, - mindist)) { + mindist)) { igraph_vs_destroy(&vs); return igraphmodule_handle_igraph_error(); } @@ -5102,7 +5117,7 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOOOO!Olf", kwlist, &vobj, &directed, &damping, &robj, - &rvsobj, &wobj, + &rvsobj, &wobj, &igraphmodule_ARPACKOptionsType, &arpack_options_o, &algo_o, &niter, &eps)) @@ -5162,10 +5177,10 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel if (rvsobj != Py_None) retval = igraph_personalized_pagerank_vs(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset_vs, &weights, opts); + PyObject_IsTrue(directed), damping, reset_vs, &weights, opts); else retval = igraph_personalized_pagerank(&self->g, algo, &res, 0, vs, - PyObject_IsTrue(directed), damping, reset, &weights, opts); + PyObject_IsTrue(directed), damping, reset, &weights, opts); if (retval) { igraphmodule_handle_igraph_error(); @@ -5885,7 +5900,7 @@ PyObject } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vs_destroy(&vs); igraph_vector_destroy(&result); return NULL; @@ -5986,7 +6001,7 @@ PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, if (source < 0 && target < 0) { if (igraph_vertex_connectivity(&self->g, &res, PyObject_IsTrue(checks))) { igraphmodule_handle_igraph_error(); - return NULL; + return NULL; } } else if (source >= 0 && target >= 0) { if (igraphmodule_PyObject_to_vconn_nei_t(neis, &neighbors)) @@ -5997,8 +6012,8 @@ PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, return NULL; } } else { - PyErr_SetString(PyExc_ValueError, "if source or target is given, the other one must also be specified"); - return NULL; + PyErr_SetString(PyExc_ValueError, "if source or target is given, the other one must also be specified"); + return NULL; } if (!IGRAPH_FINITE(res)) return Py_BuildValue("d", (double)res); @@ -6420,7 +6435,7 @@ PyObject *igraphmodule_Graph_layout_random(igraphmodule_GraphObject * self, * \sa igraph_layout_grid, igraph_layout_grid_3d */ PyObject *igraphmodule_Graph_layout_grid(igraphmodule_GraphObject* self, - PyObject *args, PyObject *kwds) { + PyObject *args, PyObject *kwds) { static char *kwlist[] = { "width", "height", "dim", NULL }; igraph_matrix_t m; @@ -6469,7 +6484,7 @@ PyObject *igraphmodule_Graph_layout_grid(igraphmodule_GraphObject* self, * \sa igraph_layout_star */ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, - PyObject *args, PyObject *kwds) { + PyObject *args, PyObject *kwds) { static char *kwlist[] = { "center", "order", NULL }; @@ -6575,7 +6590,7 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * } } else { use_seed=1; - if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) return NULL; + if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) return NULL; } /* Convert minimum and maximum x-y-z values */ @@ -6673,7 +6688,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel &weight_node_dist, &weight_border, &weight_edge_lengths, &weight_edge_crossings, &weight_node_edge_dist)) - return NULL; + return NULL; /* Provide default parameters based on the properties of the graph */ if (fineiter < 0) { @@ -6713,9 +6728,9 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel } } else { if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) { - return NULL; - } - use_seed=1; + return NULL; + } + use_seed=1; } retval = igraph_layout_davidson_harel(&self->g, &m, use_seed, @@ -6755,7 +6770,7 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOl", kwlist, &wobj, &seed_o, &fixed_o, &options_o, &dim)) - return NULL; + return NULL; if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); @@ -6768,47 +6783,47 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, if (fixed_o != 0 && fixed_o != Py_None) { fixed = (igraph_vector_bool_t*)malloc(sizeof(igraph_vector_bool_t)); if (!fixed) { - PyErr_NoMemory(); - return NULL; - } - if (igraphmodule_PyObject_to_vector_bool_t(fixed_o, fixed)) { - free(fixed); - return NULL; - } + PyErr_NoMemory(); + return NULL; + } + if (igraphmodule_PyObject_to_vector_bool_t(fixed_o, fixed)) { + free(fixed); + return NULL; + } } if (seed_o == 0 || seed_o == Py_None) { if (igraph_matrix_init(&m, 1, 1)) { igraphmodule_handle_igraph_error(); - if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } + if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } return NULL; } } else { if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) { - if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } - return NULL; - } - use_seed=1; + if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } + return NULL; + } + use_seed=1; } /* Convert the weight parameter to a vector */ if (igraphmodule_attrib_to_vector_t(wobj, self, &weights, ATTRIBUTE_TYPE_EDGE)) { igraph_matrix_destroy(&m); - if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } + if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } igraphmodule_handle_igraph_error(); return NULL; } if (dim == 2) { - retval = igraph_layout_drl(&self->g, &m, use_seed, &options, weights, fixed); + retval = igraph_layout_drl(&self->g, &m, use_seed, &options, weights, fixed); } else { - retval = igraph_layout_drl_3d(&self->g, &m, use_seed, &options, weights, fixed); + retval = igraph_layout_drl_3d(&self->g, &m, use_seed, &options, weights, fixed); } if (retval) { igraph_matrix_destroy(&m); if (weights) { igraph_vector_destroy(weights); free(weights); } - if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } + if (fixed) { igraph_vector_bool_destroy(fixed); free(fixed); } igraphmodule_handle_igraph_error(); return NULL; } @@ -6886,7 +6901,7 @@ PyObject } } else { if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) return NULL; - use_seed=1; + use_seed=1; } /* Convert the weight parameter to a vector */ @@ -6994,7 +7009,7 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, } else { use_seed=1; if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) - return NULL; + return NULL; } if (igraph_layout_graphopt(&self->g, &m, (igraph_integer_t) niter, @@ -7096,7 +7111,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, PyErr_NoMemory(); return NULL; } - if (igraphmodule_PyList_to_matrix_t(dist_o, dist)) { + if (igraphmodule_PyList_to_matrix_t(dist_o, dist)) { free(dist); return NULL; } @@ -7282,14 +7297,14 @@ PyObject *igraphmodule_Graph_layout_sugiyama( } if (igraphmodule_attrib_to_vector_t(layers_o, self, &layers, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { igraph_vector_destroy(&extd_to_orig_eids); igraph_matrix_destroy(&m); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (layers != 0) { igraph_vector_destroy(layers); free(layers); } igraph_vector_destroy(&extd_to_orig_eids); igraph_matrix_destroy(&m); @@ -7358,7 +7373,7 @@ PyObject *igraphmodule_Graph_layout_bipartite( } if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { igraph_matrix_destroy(&m); Py_DECREF(types_o); return NULL; @@ -7441,17 +7456,17 @@ PyObject *igraphmodule_Graph_get_incidence(igraphmodule_GraphObject * self, return NULL; if (igraph_vector_init(&row_ids, 0)) - return NULL; + return NULL; if (igraph_vector_init(&col_ids, 0)) { igraph_vector_destroy(&row_ids); - return NULL; + return NULL; } if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) { igraph_vector_destroy(&row_ids); igraph_vector_destroy(&col_ids); - return NULL; + return NULL; } if (igraph_matrix_init(&matrix, 1, 1)) { @@ -7504,7 +7519,7 @@ PyObject *igraphmodule_Graph_laplacian(igraphmodule_GraphObject * self, return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; if (igraph_matrix_init (&m, igraph_vcount(&self->g), igraph_vcount(&self->g))) { @@ -7514,7 +7529,7 @@ PyObject *igraphmodule_Graph_laplacian(igraphmodule_GraphObject * self, } if (igraph_laplacian(&self->g, &m, /*sparseres=*/ 0, - PyObject_IsTrue(normalized), weights)) { + PyObject_IsTrue(normalized), weights)) { igraphmodule_handle_igraph_error(); if (weights) { igraph_vector_destroy(weights); free(weights); } igraph_matrix_destroy(&m); @@ -7579,7 +7594,7 @@ PyObject *igraphmodule_Graph_to_undirected(igraphmodule_GraphObject * self, return NULL; if (igraphmodule_PyObject_to_attribute_combination_t(comb_o, &comb)) - return NULL; + return NULL; if (igraph_to_undirected(&self->g, mode, &comb)) { igraph_attribute_combination_destroy(&comb); @@ -8259,7 +8274,7 @@ PyObject *igraphmodule_Graph_write_graphml(igraphmodule_GraphObject * self, return NULL; if (igraph_write_graph_graphml(&self->g, igraphmodule_filehandle_get(&fobj), - /*prefixattr=*/ 1)) { + /*prefixattr=*/ 1)) { igraphmodule_handle_igraph_error(); igraphmodule_filehandle_destroy(&fobj); return NULL; @@ -8286,7 +8301,7 @@ PyObject *igraphmodule_Graph_write_leda(igraphmodule_GraphObject * self, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zz", kwlist, &fname, &vertex_attr_name, - &edge_attr_name)) + &edge_attr_name)) return NULL; if (igraphmodule_filehandle_init(&fobj, fname, "w")) @@ -8313,7 +8328,7 @@ PyObject *igraphmodule_Graph_write_leda(igraphmodule_GraphObject * self, * \sa igraph_canonical_permutation */ PyObject *igraphmodule_Graph_canonical_permutation( - igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "sh", "color", NULL }; PyObject *sh_o = Py_None; PyObject *color_o = Py_None; @@ -8327,24 +8342,24 @@ PyObject *igraphmodule_Graph_canonical_permutation( return NULL; if (igraphmodule_PyObject_to_bliss_sh_t(sh_o, &sh)) - return NULL; + return NULL; if (igraph_vector_init(&labeling, 0)) { - igraphmodule_handle_igraph_error(); - return NULL; + igraphmodule_handle_igraph_error(); + return NULL; } if (igraphmodule_attrib_to_vector_int_t(color_o, self, &color, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; retval = igraph_canonical_permutation(&self->g, color, &labeling, sh, 0); if (color) { igraph_vector_int_destroy(color); free(color); } if (retval) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&labeling); - return NULL; + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&labeling); + return NULL; } list = igraphmodule_vector_t_to_PyList(&labeling, IGRAPHMODULE_TYPE_INT); @@ -8449,7 +8464,7 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, int retval; static char *kwlist[] = { "other", "return_mapping_12", - "return_mapping_21", "sh1", "sh2", "color1", "color2", NULL }; + "return_mapping_21", "sh1", "sh2", "color1", "color2", NULL }; /* TODO: convert igraph_bliss_info_t when needed */ if (!PyArg_ParseTupleAndKeywords (args, kwds, "|O!OOOOOO", kwlist, &igraphmodule_GraphType, &o, @@ -8465,23 +8480,23 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, sh2 = sh1; if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, self, &color2, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (o == Py_None) other = self; else other = (igraphmodule_GraphObject *) o; if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); - map12 = &mapping_12; + igraph_vector_init(&mapping_12, 0); + map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); - map21 = &mapping_21; + igraph_vector_init(&mapping_21, 0); + map21 = &mapping_21; } retval = igraph_isomorphic_bliss(&self->g, &other->g, color1, color2, - &result, map12, map21, sh1, 0, 0); + &result, map12, map21, sh1, 0, 0); if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } @@ -8495,27 +8510,27 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, if (result) Py_RETURN_TRUE; Py_RETURN_FALSE; } else { - PyObject *iso, *m1, *m2; - iso = result ? Py_True : Py_False; - Py_INCREF(iso); - if (map12) { - m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map12); - if (!m1) { - Py_DECREF(iso); - if (map21) igraph_vector_destroy(map21); - return NULL; - } - } else { m1 = Py_None; Py_INCREF(m1); } - if (map21) { - m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map21); - if (!m2) { - Py_DECREF(iso); Py_DECREF(m1); - return NULL; - } - } else { m2 = Py_None; Py_INCREF(m2); } - return Py_BuildValue("NNN", iso, m1, m2); + PyObject *iso, *m1, *m2; + iso = result ? Py_True : Py_False; + Py_INCREF(iso); + if (map12) { + m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(map12); + if (!m1) { + Py_DECREF(iso); + if (map21) igraph_vector_destroy(map21); + return NULL; + } + } else { m1 = Py_None; Py_INCREF(m1); } + if (map21) { + m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(map21); + if (!m2) { + Py_DECREF(iso); Py_DECREF(m1); + return NULL; + } + } else { m2 = Py_None; Py_INCREF(m2); } + return Py_BuildValue("NNN", iso, m1, m2); } } @@ -8676,20 +8691,20 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, } if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, other, &color2, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color1_o, self, &edge_color1, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color2_o, other, &edge_color2, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } @@ -8697,12 +8712,12 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); - map12 = &mapping_12; + igraph_vector_init(&mapping_12, 0); + map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); - map21 = &mapping_21; + igraph_vector_init(&mapping_21, 0); + map21 = &mapping_21; } callback_data.graph1 = (PyObject*)self; @@ -8740,24 +8755,24 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, if (result) Py_RETURN_TRUE; Py_RETURN_FALSE; } else { - PyObject *m1, *m2; - if (map12) { - m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map12); - if (!m1) { - if (map21) igraph_vector_destroy(map21); - return NULL; - } - } else { m1 = Py_None; Py_INCREF(m1); } - if (map21) { - m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map21); - if (!m2) { - Py_DECREF(m1); - return NULL; - } - } else { m2 = Py_None; Py_INCREF(m2); } - return Py_BuildValue("ONN", result ? Py_True : Py_False, m1, m2); + PyObject *m1, *m2; + if (map12) { + m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(map12); + if (!m1) { + if (map21) igraph_vector_destroy(map21); + return NULL; + } + } else { m1 = Py_None; Py_INCREF(m1); } + if (map21) { + m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(map21); + if (!m2) { + Py_DECREF(m1); + return NULL; + } + } else { m2 = Py_None; Py_INCREF(m2); } + return Py_BuildValue("ONN", result ? Py_True : Py_False, m1, m2); } } @@ -8806,20 +8821,20 @@ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *se } if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, other, &color2, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color1_o, self, &edge_color1, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color2_o, other, &edge_color2, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } @@ -8899,20 +8914,20 @@ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self } if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, other, &color2, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color1_o, self, &edge_color1, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color2_o, other, &edge_color2, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } @@ -9011,20 +9026,20 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, } if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, other, &color2, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color1_o, self, &edge_color1, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color2_o, other, &edge_color2, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } @@ -9032,12 +9047,12 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); - map12 = &mapping_12; + igraph_vector_init(&mapping_12, 0); + map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); - map21 = &mapping_21; + igraph_vector_init(&mapping_21, 0); + map21 = &mapping_21; } callback_data.graph1 = (PyObject*)self; @@ -9076,28 +9091,28 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, Py_RETURN_TRUE; Py_RETURN_FALSE; } else { - PyObject *m1, *m2; - if (map12) { - m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map12); - if (!m1) { - if (map21) igraph_vector_destroy(map21); - return NULL; - } - } else { + PyObject *m1, *m2; + if (map12) { + m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(map12); + if (!m1) { + if (map21) igraph_vector_destroy(map21); + return NULL; + } + } else { m1 = Py_None; Py_INCREF(m1); } - if (map21) { - m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map21); - if (!m2) { - Py_DECREF(m1); - return NULL; - } - } else { + if (map21) { + m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(map21); + if (!m2) { + Py_DECREF(m1); + return NULL; + } + } else { m2 = Py_None; Py_INCREF(m2); } - return Py_BuildValue("ONN", result ? Py_True : Py_False, m1, m2); + return Py_BuildValue("ONN", result ? Py_True : Py_False, m1, m2); } } @@ -9143,20 +9158,20 @@ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject } if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, other, &color2, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color1_o, self, &edge_color1, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color2_o, other, &edge_color2, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } @@ -9237,20 +9252,20 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *s } if (igraphmodule_attrib_to_vector_int_t(color1_o, self, &color1, - ATTRIBUTE_TYPE_VERTEX)) return NULL; + ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_int_t(color2_o, other, &color2, - ATTRIBUTE_TYPE_VERTEX)) { + ATTRIBUTE_TYPE_VERTEX)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color1_o, self, &edge_color1, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } return NULL; } if (igraphmodule_attrib_to_vector_int_t(edge_color2_o, other, &edge_color2, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { if (color1) { igraph_vector_int_destroy(color1); free(color1); } if (color2) { igraph_vector_int_destroy(color2); free(color2); } if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } @@ -9325,13 +9340,13 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return_mapping)) { - if (igraph_vector_init(&mapping, 0)) { + if (igraph_vector_init(&mapping, 0)) { if (p_domains) igraph_vector_ptr_destroy_all(p_domains); igraphmodule_handle_igraph_error(); return NULL; } - map = &mapping; + map = &mapping; } if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, &result, @@ -9350,11 +9365,11 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, Py_RETURN_TRUE; Py_RETURN_FALSE; } else { - PyObject *m = igraphmodule_vector_t_to_PyList(map, IGRAPHMODULE_TYPE_INT); + PyObject *m = igraphmodule_vector_t_to_PyList(map, IGRAPHMODULE_TYPE_INT); igraph_vector_destroy(map); - if (!m) - return NULL; - return Py_BuildValue("ON", result ? Py_True : Py_False, m); + if (!m) + return NULL; + return Py_BuildValue("ON", result ? Py_True : Py_False, m); } } @@ -9639,7 +9654,7 @@ PyObject *igraphmodule_Graph_compose(igraphmodule_GraphObject * self, o = (igraphmodule_GraphObject *) other; if (igraph_compose(&g, &self->g, &o->g, /*edge_map1=*/ 0, - /*edge_map2=*/ 0)) { + /*edge_map2=*/ 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -9875,7 +9890,7 @@ PyObject *igraphmodule_Graph_maxflow_value(igraphmodule_GraphObject * self, if (igraph_maxflow_value(&self->g, &result, v1, v2, &capacity_vector, - &stats)) { + &stats)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } @@ -9929,7 +9944,7 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, } if (igraph_maxflow(&self->g, &result, &flow, &cut, &partition, 0, - v1, v2, &capacity_vector, &stats)) { + v1, v2, &capacity_vector, &stats)) { igraph_vector_destroy(&capacity_vector); igraph_vector_destroy(&flow); igraph_vector_destroy(&cut); @@ -9991,11 +10006,11 @@ PyObject *igraphmodule_Graph_all_st_cuts(igraphmodule_GraphObject * self, return NULL; if (igraph_vector_ptr_init(&partition1s, 0)) { - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (igraph_vector_ptr_init(&cuts, 0)) { igraph_vector_ptr_destroy(&partition1s); - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (igraph_all_st_cuts(&self->g, &cuts, &partition1s, @@ -10047,11 +10062,11 @@ PyObject *igraphmodule_Graph_all_st_mincuts(igraphmodule_GraphObject * self, return NULL; if (igraph_vector_ptr_init(&partition1s, 0)) { - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (igraph_vector_ptr_init(&cuts, 0)) { igraph_vector_ptr_destroy(&partition1s); - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (igraphmodule_PyObject_to_attribute_values(capacity_o, @@ -10195,18 +10210,18 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, if (igraph_vector_init(&partition, 0)) { igraph_vector_destroy(&capacity_vector); - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (igraph_vector_init(&partition2, 0)) { igraph_vector_destroy(&partition); igraph_vector_destroy(&capacity_vector); - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (igraph_vector_init(&cut, 0)) { igraph_vector_destroy(&partition); igraph_vector_destroy(&partition2); igraph_vector_destroy(&capacity_vector); - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); } if (source == -1 && target == -1) { @@ -10238,7 +10253,7 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, if (!cut_o) { igraph_vector_destroy(&partition); igraph_vector_destroy(&partition2); - return 0; + return 0; } part_o=igraphmodule_vector_t_to_PyList(&partition, IGRAPHMODULE_TYPE_INT); @@ -10253,7 +10268,7 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, igraph_vector_destroy(&partition2); if (!part2_o) { Py_DECREF(part_o); - Py_DECREF(cut_o); + Py_DECREF(cut_o); return 0; } @@ -10712,11 +10727,11 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject eps = DBL_EPSILON * 1000; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) - return NULL; + return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { if (types != 0) { igraph_vector_bool_destroy(types); free(types); } - return NULL; + return NULL; } if (igraph_vector_long_init(&result, 0)) { @@ -11081,7 +11096,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject return NULL; if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) return NULL; + ATTRIBUTE_TYPE_EDGE)) return NULL; if (igraph_matrix_init(&merges, 0, 0)) { if (weights != 0) { @@ -11181,7 +11196,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj n -= 1; if (igraphmodule_attrib_to_vector_t(weights_obj, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_matrix_destroy(&m); igraph_vector_destroy(&members); return NULL; @@ -11293,7 +11308,7 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, } if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { - igraphmodule_handle_igraph_error(); + igraphmodule_handle_igraph_error(); return NULL; } @@ -11315,8 +11330,8 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, /*e_weight=*/ e_ws, /*v_weight=*/ v_ws, /*nb_trials=*/nb_trials, /*out*/ &membership, &codelength)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&membership); if (e_ws) { igraph_vector_destroy(e_ws); free(e_ws); @@ -11342,7 +11357,7 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, igraph_vector_destroy(&membership); if (!res) - return NULL; + return NULL; return Py_BuildValue("Nd", res, (double)codelength); } @@ -11465,7 +11480,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self * Optimal modularity by integer programming */ PyObject *igraphmodule_Graph_community_optimal_modularity( - igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"weights", NULL}; PyObject *weights_o = Py_None; igraph_real_t modularity; @@ -11478,20 +11493,20 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( return NULL; if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { - igraphmodule_handle_igraph_error(); + igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vector_destroy(&membership); return NULL; } if (igraph_community_optimal_modularity(&self->g, &modularity, &membership, weights)) { - igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&membership); if (weights != 0) { igraph_vector_destroy(weights); free(weights); } @@ -11506,7 +11521,7 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( igraph_vector_destroy(&membership); if (!res) - return NULL; + return NULL; return Py_BuildValue("Nd", res, (double)modularity); } @@ -11549,12 +11564,12 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, } if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { - igraphmodule_handle_igraph_error(); + igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + ATTRIBUTE_TYPE_EDGE)) { igraph_vector_destroy(&membership); return NULL; } @@ -11784,24 +11799,24 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, igraph_vector_t walk; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiOO", kwlist, &start_o, - &steps, &mode_o, &stuck_o)) + &steps, &mode_o, &stuck_o)) return NULL; if (igraphmodule_PyObject_to_vid(start_o, &start, &self->g)) - return NULL; + return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; + return NULL; if (igraphmodule_PyObject_to_random_walk_stuck_t(stuck_o, &stuck)) - return NULL; + return NULL; if (igraph_vector_init(&walk, steps)) - return igraphmodule_handle_igraph_error(); + return igraphmodule_handle_igraph_error(); if (igraph_random_walk(&self->g, &walk, start, mode, steps, stuck)) { - igraph_vector_destroy(&walk); - return igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&walk); + return igraphmodule_handle_igraph_error(); } res = igraphmodule_vector_t_to_PyList(&walk, IGRAPHMODULE_TYPE_INT); @@ -12296,17 +12311,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param loops: whether self-loops are allowed.\n"}, /* interface to igraph_famous */ - {"Famous", (PyCFunction) igraphmodule_Graph_Famous, - METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Famous(name)\n\n" - "Generates a famous graph based on its name.\n\n" - "Several famous graphs are known to C{igraph} including (but not limited to)\n" - "the Chvatal graph, the Petersen graph or the Tutte graph. This method\n" - "generates one of them based on its name (case insensitive). See the\n" - "documentation of the C interface of C{igraph} for the names available:\n" - "U{http://igraph.org/doc/c}.\n\n" - "@param name: the name of the graph to be generated.\n" - }, + {"Famous", (PyCFunction) igraphmodule_Graph_Famous, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Famous(name)\n\n" + "Generates a famous graph based on its name.\n\n" + "Several famous graphs are known to C{igraph} including (but not limited to)\n" + "the Chvatal graph, the Petersen graph or the Tutte graph. This method\n" + "generates one of them based on its name (case insensitive). See the\n" + "documentation of the C interface of C{igraph} for the names available:\n" + "U{http://igraph.org/doc/c}.\n\n" + "@param name: the name of the graph to be generated.\n" + }, /* interface to igraph_forest_fire_game */ {"Forest_Fire", (PyCFunction) igraphmodule_Graph_Forest_Fire, @@ -12501,20 +12516,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param circular: whether the generated lattice is periodic.\n"}, /* interface to igraph_lcf */ - {"LCF", (PyCFunction) igraphmodule_Graph_LCF, - METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "LCF(n, shifts, repeats)\n\n" - "Generates a graph from LCF notation.\n\n" - "LCF is short for Lederberg-Coxeter-Frucht, it is a concise notation\n" - "for 3-regular Hamiltonian graphs. It consists of three parameters,\n" - "the number of vertices in the graph, a list of shifts giving\n" - "additional edges to a cycle backbone and another integer giving how\n" - "many times the shifts should be performed. See\n" - "U{http://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" - "@param n: the number of vertices\n" - "@param shifts: the shifts in a list or tuple\n" - "@param repeats: the number of repeats\n" - }, + {"LCF", (PyCFunction) igraphmodule_Graph_LCF, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "LCF(n, shifts, repeats)\n\n" + "Generates a graph from LCF notation.\n\n" + "LCF is short for Lederberg-Coxeter-Frucht, it is a concise notation\n" + "for 3-regular Hamiltonian graphs. It consists of three parameters,\n" + "the number of vertices in the graph, a list of shifts giving\n" + "additional edges to a cycle backbone and another integer giving how\n" + "many times the shifts should be performed. See\n" + "U{http://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" + "@param n: the number of vertices\n" + "@param shifts: the shifts in a list or tuple\n" + "@param repeats: the number of repeats\n" + }, // interface to igraph_ring {"Ring", (PyCFunction) igraphmodule_Graph_Ring, @@ -13076,9 +13091,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: a list with the (exact or estimated) edge betweennesses of all\n" " edges.\n"}, - {"eigen_adjacency", (PyCFunction) igraphmodule_Graph_eigen_adjacency, - METH_VARARGS | METH_KEYWORDS, - "" }, + {"eigen_adjacency", (PyCFunction) igraphmodule_Graph_eigen_adjacency, + METH_VARARGS | METH_KEYWORDS, + "" }, /* interface to igraph_[st_]edge_connectivity */ {"edge_connectivity", (PyCFunction) igraphmodule_Graph_edge_connectivity, @@ -13239,14 +13254,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"convergence_degree", (PyCFunction)igraphmodule_Graph_convergence_degree, METH_NOARGS, "convergence_degree()\n\n" - "Undocumented (yet)." + "Undocumented (yet)." }, /* interface to igraph_convergence_field_size */ {"convergence_field_size", (PyCFunction)igraphmodule_Graph_convergence_field_size, METH_NOARGS, "convergence_field_size()\n\n" - "Undocumented (yet)." + "Undocumented (yet)." }, /* interface to igraph_hub_score */ diff --git a/tests/test_structural.py b/tests/test_structural.py index 5fecf4f43..5e2a1510c 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -26,8 +26,8 @@ def testDensity(self): def testDiameter(self): self.assertTrue(self.gfull.diameter() == 1) - self.assertTrue(isnan(self.gempty.diameter(unconn=False))) - self.assertTrue(isnan(self.gempty.diameter(unconn=False, weights=[]))) + self.assertTrue(self.gempty.diameter(unconn=False) == inf) + self.assertTrue(self.gempty.diameter(unconn=False) == inf) self.assertTrue(self.g.diameter() == 2) self.assertTrue(self.gdir.diameter(False) == 2) self.assertTrue(self.gdir.diameter() == 3) @@ -35,7 +35,7 @@ def testDiameter(self): s, t, d = self.tree.farthest_points() self.assertTrue((s == 13 or t == 13) and d == 5) - self.assertTrue(self.gempty.farthest_points(unconn=False) == (None, None, 10)) + self.assertTrue(self.gempty.farthest_points(unconn=False) == (None, None, inf)) d = self.tree.get_diameter() self.assertTrue(d[0] == 13 or d[-1] == 13) @@ -262,7 +262,7 @@ def testEdgeBetweennessCentrality(self): def testClosenessCentrality(self): g = Graph.Star(5) cl = g.closeness() - cl2 = [1.0, 0.57142, 0.57142, 0.57142, 0.57142] + cl2 = [1.0, 4 / 7.0, 4 / 7.0, 4 / 7.0, 4 / 7.0] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) From 9f7c150088c0d1d867783ccae65f6ea4b3a82bb2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 26 Jan 2021 16:37:21 +0100 Subject: [PATCH 0102/1681] fix: fix crashes when calling certain attribute combination functions with an empty vector, closes #358 --- src/_igraph/attributes.c | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index db746334c..0509a1e88 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -994,10 +994,7 @@ static PyObject* igraphmodule_i_ac_first(PyObject* values, igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; long int n = igraph_vector_size(v); - if (n == 0) - continue; - - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[0]); + item = n > 0 ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; Py_INCREF(item); PyList_SET_ITEM(res, i, item); /* reference to item stolen */ } @@ -1032,19 +1029,20 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; long int n = igraph_vector_size(v); - if (n == 0) - continue; - - num = PyObject_CallObject(random_func, 0); - if (num == 0) { - Py_DECREF(random_func); - Py_DECREF(res); - return 0; + if (n == 0) { + num = PyObject_CallObject(random_func, 0); + if (num == 0) { + Py_DECREF(random_func); + Py_DECREF(res); + return 0; + } + item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[(long int)(n*PyFloat_AsDouble(num))]); + Py_DECREF(num); + } else { + item = Py_None; } - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[(long int)(n*PyFloat_AsDouble(num))]); Py_INCREF(item); - Py_DECREF(num); PyList_SET_ITEM(res, i, item); /* reference to item stolen */ } @@ -1069,10 +1067,7 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; long int n = igraph_vector_size(v); - if (n == 0) - continue; - - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[n-1]); + item = (n > 0) ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; Py_INCREF(item); PyList_SET_ITEM(res, i, item); /* reference to item stolen */ } @@ -1143,8 +1138,12 @@ static PyObject* igraphmodule_i_ac_median(PyObject* values, Py_DECREF(res); return 0; } - if (n % 2 == 1) { + if (n == 0) { + item = Py_None; + Py_INCREF(item); + } else if (n % 2 == 1) { item = PyList_GET_ITEM(list, n / 2); + Py_INCREF(item); } else { igraph_real_t num1, num2; item = PyList_GET_ITEM(list, n / 2 - 1); From 692e82ef1dec1f56b3bf4ae13d6da99cf46670f9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 26 Jan 2021 16:38:31 +0100 Subject: [PATCH 0103/1681] format: src/_igraph/attributes.c reformatted without tabs --- src/_igraph/attributes.c | 64 ++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 0509a1e88..0bfe8d269 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -1349,12 +1349,12 @@ static int igraphmodule_i_attribute_combine_edges(const igraph_t *graph, /* Getting attribute names and types */ static int igraphmodule_i_attribute_get_info(const igraph_t *graph, - igraph_strvector_t *gnames, - igraph_vector_t *gtypes, - igraph_strvector_t *vnames, - igraph_vector_t *vtypes, - igraph_strvector_t *enames, - igraph_vector_t *etypes) { + igraph_strvector_t *gnames, + igraph_vector_t *gtypes, + igraph_strvector_t *vnames, + igraph_vector_t *vtypes, + igraph_strvector_t *enames, + igraph_vector_t *etypes) { igraph_strvector_t *names[3] = { gnames, vnames, enames }; igraph_vector_t *types[3] = { gtypes, vtypes, etypes }; int retval; @@ -1427,8 +1427,8 @@ static int igraphmodule_i_attribute_get_info(const igraph_t *graph, /* Checks whether the graph has a graph/vertex/edge attribute with the given name */ igraph_bool_t igraphmodule_i_attribute_has_attr(const igraph_t *graph, - igraph_attribute_elemtype_t type, - const char* name) { + igraph_attribute_elemtype_t type, + const char* name) { switch (type) { case IGRAPH_ATTRIBUTE_GRAPH: return igraphmodule_has_graph_attribute(graph, name); @@ -1443,9 +1443,9 @@ igraph_bool_t igraphmodule_i_attribute_has_attr(const igraph_t *graph, /* Returns the type of a given attribute */ int igraphmodule_i_attribute_get_type(const igraph_t *graph, - igraph_attribute_type_t *type, - igraph_attribute_elemtype_t elemtype, - const char *name) { + igraph_attribute_type_t *type, + igraph_attribute_elemtype_t elemtype, + const char *name) { long int attrnum, i, j; int is_numeric, is_string, is_boolean; PyObject *o, *dict; @@ -1511,7 +1511,7 @@ int igraphmodule_i_attribute_get_type(const igraph_t *graph, /* Getting Boolean graph attributes */ int igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, - const char *name, igraph_vector_bool_t *value) { + const char *name, igraph_vector_bool_t *value) { PyObject *dict, *o; dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_GRAPH]; /* No error checking, if we get here, the type has already been checked by previous @@ -1526,7 +1526,7 @@ int igraphmodule_i_get_boolean_graph_attr(const igraph_t *graph, /* Getting numeric graph attributes */ int igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, - const char *name, igraph_vector_t *value) { + const char *name, igraph_vector_t *value) { PyObject *dict, *o, *result; dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_GRAPH]; /* No error checking, if we get here, the type has already been checked by previous @@ -1549,7 +1549,7 @@ int igraphmodule_i_get_numeric_graph_attr(const igraph_t *graph, /* Getting string graph attributes */ int igraphmodule_i_get_string_graph_attr(const igraph_t *graph, - const char *name, igraph_strvector_t *value) { + const char *name, igraph_strvector_t *value) { PyObject *dict, *o, *str = 0; const char* c_str; @@ -1603,9 +1603,9 @@ int igraphmodule_i_get_string_graph_attr(const igraph_t *graph, /* Getting numeric vertex attributes */ int igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, - const char *name, - igraph_vs_t vs, - igraph_vector_t *value) { + const char *name, + igraph_vs_t vs, + igraph_vector_t *value) { PyObject *dict, *list, *result, *o; igraph_vector_t newvalue; @@ -1643,9 +1643,9 @@ int igraphmodule_i_get_numeric_vertex_attr(const igraph_t *graph, /* Getting string vertex attributes */ int igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, - const char *name, - igraph_vs_t vs, - igraph_strvector_t *value) { + const char *name, + igraph_vs_t vs, + igraph_strvector_t *value) { PyObject *dict, *list, *result; igraph_strvector_t newvalue; @@ -1697,9 +1697,9 @@ int igraphmodule_i_get_string_vertex_attr(const igraph_t *graph, /* Getting boolean vertex attributes */ int igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, - const char *name, - igraph_vs_t vs, - igraph_vector_bool_t *value) { + const char *name, + igraph_vs_t vs, + igraph_vector_bool_t *value) { PyObject *dict, *list, *o; igraph_vector_bool_t newvalue; @@ -1733,9 +1733,9 @@ int igraphmodule_i_get_boolean_vertex_attr(const igraph_t *graph, /* Getting numeric edge attributes */ int igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, - const char *name, - igraph_es_t es, - igraph_vector_t *value) { + const char *name, + igraph_es_t es, + igraph_vector_t *value) { PyObject *dict, *list, *result, *o; igraph_vector_t newvalue; @@ -1773,9 +1773,9 @@ int igraphmodule_i_get_numeric_edge_attr(const igraph_t *graph, /* Getting string edge attributes */ int igraphmodule_i_get_string_edge_attr(const igraph_t *graph, - const char *name, - igraph_es_t es, - igraph_strvector_t *value) { + const char *name, + igraph_es_t es, + igraph_strvector_t *value) { PyObject *dict, *list, *result; igraph_strvector_t newvalue; @@ -1825,9 +1825,9 @@ int igraphmodule_i_get_string_edge_attr(const igraph_t *graph, /* Getting boolean edge attributes */ int igraphmodule_i_get_boolean_edge_attr(const igraph_t *graph, - const char *name, - igraph_es_t es, - igraph_vector_bool_t *value) { + const char *name, + igraph_es_t es, + igraph_vector_bool_t *value) { PyObject *dict, *list, *o; igraph_vector_bool_t newvalue; From b71d7af49bfc3330de450f85f0c9a497ee9a58ff Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 28 Jan 2021 14:48:25 +0100 Subject: [PATCH 0104/1681] fix: fix 'random' attribute combination that I broke in the previous commit --- src/_igraph/attributes.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 0bfe8d269..f0a027fa1 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -1029,7 +1029,7 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; long int n = igraph_vector_size(v); - if (n == 0) { + if (n > 0) { num = PyObject_CallObject(random_func, 0); if (num == 0) { Py_DECREF(random_func); From e5adcf0891de6c9c827a3f68a3fcceb6569f467b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 28 Jan 2021 14:49:17 +0100 Subject: [PATCH 0105/1681] chore: bumped to latest igraph development version --- src/_igraph/graphobject.c | 4 ++-- tests/test_structural.py | 4 ++-- vendor/source/igraph | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 73ef24061..bab3391ac 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4079,7 +4079,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, } if (cutoff == Py_None) { - if (igraph_closeness(&self->g, &res, vs, mode, weights, + if (igraph_closeness(&self->g, &res, 0, 0, vs, mode, weights, PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); @@ -4093,7 +4093,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); igraph_vector_destroy(&res); return NULL; } - if (igraph_closeness_cutoff(&self->g, &res, vs, mode, weights, + if (igraph_closeness_cutoff(&self->g, &res, 0, 0, vs, mode, weights, (igraph_real_t)PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); diff --git a/tests/test_structural.py b/tests/test_structural.py index 5e2a1510c..f001bf37a 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -269,7 +269,7 @@ def testClosenessCentrality(self): g = Graph.Star(5) with warnings.catch_warnings(): warnings.simplefilter("ignore") - cl = g.closeness(cutoff=0.9) + cl = g.closeness(cutoff=1.0) cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -285,7 +285,7 @@ def testClosenessCentrality(self): g = Graph.Star(5) with warnings.catch_warnings(): warnings.simplefilter("ignore") - cl = g.closeness(cutoff=0.9, weights=weights) + cl = g.closeness(cutoff=1.0, weights=weights) cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) diff --git a/vendor/source/igraph b/vendor/source/igraph index 1d422e68c..869be278f 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 1d422e68c735888fff755e23912cc9dac269ce40 +Subproject commit 869be278ffa5bc97e3cb7b08f58d2b111c9dd47b From 9e632a44ddc72f5f8446eb4095787097fbf97419 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 28 Jan 2021 18:05:43 +0100 Subject: [PATCH 0106/1681] fix: unit tests now pass again --- tests/test_basic.py | 15 +++++++++++++++ tests/test_structural.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index 469506597..67367d504 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -725,24 +725,39 @@ def __new__(cls, *args, **kwds): result.new_called = True return result + @classmethod + def Adjacency(cls, *args, **kwds): + result = super(InheritedGraph, cls).Adjacency(*args, **kwds) + result.adjacency_called = True + return result + class InheritanceTests(unittest.TestCase): def testInitCalledProperly(self): g = InheritedGraph() + self.assertTrue(isinstance(g, InheritedGraph)) self.assertTrue(getattr(g, "init_called", False)) def testNewCalledProperly(self): g = InheritedGraph() + self.assertTrue(isinstance(g, InheritedGraph)) self.assertTrue(getattr(g, "new_called", False)) def testInitCalledProperlyWithClassMethod(self): g = InheritedGraph.Tree(3, 2) + self.assertTrue(isinstance(g, InheritedGraph)) self.assertTrue(getattr(g, "init_called", False)) def testNewCalledProperlyWithClassMethod(self): g = InheritedGraph.Tree(3, 2) + self.assertTrue(isinstance(g, InheritedGraph)) self.assertTrue(getattr(g, "new_called", False)) + def testCallingClassMethodInSuperclass(self): + g = InheritedGraph.Adjacency([[0, 1, 1], [1, 0, 0], [1, 0, 0]]) + self.assertTrue(isinstance(g, InheritedGraph)) + self.assertTrue(getattr(g, "adjacency_called", True)) + def suite(): basic_suite = unittest.makeSuite(BasicTests) diff --git a/tests/test_structural.py b/tests/test_structural.py index f001bf37a..1b5e25618 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -270,7 +270,7 @@ def testClosenessCentrality(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.closeness(cutoff=1.0) - cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] + cl2 = [1.0, 1.0, 1.0, 1.0, 1.0] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -286,7 +286,7 @@ def testClosenessCentrality(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.closeness(cutoff=1.0, weights=weights) - cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] + cl2 = [1.0, 1.0, 1.0, 1.0, 1.0] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) From 68cf55271447342c1a75ce81cbd22fd5747c4431 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 23 Jan 2021 19:43:05 +1100 Subject: [PATCH 0107/1681] Try github actions --- .github/workflows/build.yml | 136 ++++++++++++++++++++++++++++++++++++ tox.ini | 8 +++ 2 files changed, 144 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..5c1bf328f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,136 @@ +name: "Build and deploy" + +on: + - push + - pull_request + +jobs: + build_linux: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + - pypy-3.7 + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install OS dependencies + run: + sudo apt-get install gfortran flex bison + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + + build_osx: + runs-on: macos-latest + strategy: + matrix: + python-version: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + + steps: + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install tox tox-gh-actions + - name: Test with tox + run: tox + + wheels_linux: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + - pypy-3.7 + needs: build_linux + if: startsWith(${{ github.ref }}, 'refs/tags') + steps: + - name: Wheels (linux) + env: + - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" + - CIBW_TEST_COMMAND="cd {project} && python -m unittest" + - CIBW_SKIP="cp35-*" + run: | + wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 + sudo python3 -m pip install cibuildwheel==1.6.1 + python setup.py -q sdist + python3 -m cibuildwheel --output-dir wheelhouse + + wheels_osx: + runs-on: macos-latest + strategy: + matrix: + python-version: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + + needs: build_osx + if: startsWith(${{ github.ref }}, 'refs/tags') + steps: + - name: Wheels (OSX) + run: | + python3 -m pip install cibuildwheel==1.6.1 + python3 -m cibuildwheel --output-dir wheelhouse + env: + - CIBW_BEFORE_BUILD="python setup.py build_c_core" + - CIBW_TEST_COMMAND="cd {project} && python -m unittest" + + create_release: + runs-on: ubuntu-latest + needs: + - wheels_linux + - wheels_osx + if: startsWith(${{ github.ref }}, 'refs/tags') + steps: + - name: Config deploy + run: | + git config --local user.name "ntamas" + git config --local user.email "ntamas@gmail.com" + + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: true + prerelease: false + + - name: Upload binaries and source + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' + diff --git a/tox.ini b/tox.ini index e70312f9f..212abe28b 100644 --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,14 @@ [tox] envlist = py36, py37, py38, py39, pypy, pypy3 +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + pypy-3.7: pypy3 + [testenv] commands = python -m unittest deps = From b09aa0ce345eae2448aee5662944d5de11f002fc Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 23 Jan 2021 19:47:23 +1100 Subject: [PATCH 0108/1681] Env var syntax --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c1bf328f..1d4a120dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,9 +70,9 @@ jobs: steps: - name: Wheels (linux) env: - - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - - CIBW_TEST_COMMAND="cd {project} && python -m unittest" - - CIBW_SKIP="cp35-*" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_SKIP: "cp35-*" run: | wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 sudo python3 -m pip install cibuildwheel==1.6.1 @@ -97,8 +97,8 @@ jobs: python3 -m pip install cibuildwheel==1.6.1 python3 -m cibuildwheel --output-dir wheelhouse env: - - CIBW_BEFORE_BUILD="python setup.py build_c_core" - - CIBW_TEST_COMMAND="cd {project} && python -m unittest" + CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" create_release: runs-on: ubuntu-latest From 948e303ebe0a714dd20d4e54d209686bbfd1e824 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 23 Jan 2021 19:51:20 +1100 Subject: [PATCH 0109/1681] Syntax in workflow --- .github/workflows/build.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d4a120dd..ebc5203b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -129,8 +129,5 @@ jobs: GITHUB_TOKEN: ${{ github.token }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' + asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' From 8a820c8e6eae0c49fb5edf25ca229112393b97c3 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 23 Jan 2021 19:59:35 +1100 Subject: [PATCH 0110/1681] C core submodule --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebc5203b8..798cc326c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,8 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Install C core submodule + run: git submodule update --init - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -44,6 +46,8 @@ jobs: steps: - uses: actions/checkout@v1 + - name: Install C core submodule + run: git submodule update --init - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From 6fd63f87fedc2fbed202db917379c18e5add893f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 23 Jan 2021 20:21:31 +1100 Subject: [PATCH 0111/1681] Jobs -> steps --- .github/workflows/build.yml | 50 +++++++++---------------------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 798cc326c..8a59b01a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,17 @@ jobs: pip install tox tox-gh-actions - name: Test with tox run: tox + - name: Wheels (linux) + if: startsWith(${{ github.ref }}, "refs/tags") + env: + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_SKIP: "cp35-*" + run: | + wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 + sudo python3 -m pip install cibuildwheel==1.6.1 + python setup.py -q sdist + python3 -m cibuildwheel --output-dir wheelhouse build_osx: runs-on: macos-latest @@ -58,45 +69,8 @@ jobs: pip install tox tox-gh-actions - name: Test with tox run: tox - - wheels_linux: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - pypy-3.7 - needs: build_linux - if: startsWith(${{ github.ref }}, 'refs/tags') - steps: - - name: Wheels (linux) - env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - CIBW_SKIP: "cp35-*" - run: | - wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - sudo python3 -m pip install cibuildwheel==1.6.1 - python setup.py -q sdist - python3 -m cibuildwheel --output-dir wheelhouse - - wheels_osx: - runs-on: macos-latest - strategy: - matrix: - python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - needs: build_osx - if: startsWith(${{ github.ref }}, 'refs/tags') - steps: - name: Wheels (OSX) + if: startsWith(${{ github.ref }}, 'refs/tags') run: | python3 -m pip install cibuildwheel==1.6.1 python3 -m cibuildwheel --output-dir wheelhouse From a88c117e9bf893e787921bca872f71bba0e72e1d Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 23 Jan 2021 20:22:17 +1100 Subject: [PATCH 0112/1681] job name typo --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a59b01a4..d6fa4078a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,8 +81,8 @@ jobs: create_release: runs-on: ubuntu-latest needs: - - wheels_linux - - wheels_osx + - build_linux + - build_osx if: startsWith(${{ github.ref }}, 'refs/tags') steps: - name: Config deploy From 53abeee556009530703ae6f68e7fe1fe6aea898a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 14:39:26 +1100 Subject: [PATCH 0113/1681] pip, pypy, and cbuildwheel --- .github/workflows/build.yml | 39 ++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6fa4078a..b8d19ddb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Install C core submodule + - name: Init C core submodule run: git submodule update --init - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -29,8 +29,12 @@ jobs: sudo apt-get install gfortran flex bison - name: Install dependencies run: | + # Pypi has no pip by default, and ubuntu blocks python -m ensurepip + # However, Github runners are supposed to have pip installed by default + # https://docs.github.com/en/actions/guides/building-and-testing-python + #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions cbuildwheel=1.6.1 - name: Test with tox run: tox - name: Wheels (linux) @@ -40,10 +44,19 @@ jobs: CIBW_TEST_COMMAND: "cd {project} && python -m unittest" CIBW_SKIP: "cp35-*" run: | - wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - sudo python3 -m pip install cibuildwheel==1.6.1 python setup.py -q sdist - python3 -m cibuildwheel --output-dir wheelhouse + python -m cibuildwheel --output-dir wheelhouse + - name: Upload source dist + uses: actions/upload-artifact@v2 + with: + name: source-dist + path: dist/python-igraph-*.tar.gz + - name: Upload linux wheels + uses: actions/upload-artifact@v2 + with: + name: linux-wheels + path: wheelhouse/*.whl + build_osx: runs-on: macos-latest @@ -57,7 +70,7 @@ jobs: steps: - uses: actions/checkout@v1 - - name: Install C core submodule + - name: Init C core submodule run: git submodule update --init - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 @@ -66,24 +79,28 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions + pip install tox tox-gh-actions cbuildwheels=1.6.1 - name: Test with tox run: tox - name: Wheels (OSX) - if: startsWith(${{ github.ref }}, 'refs/tags') + if: startsWith(${{ github.ref }}, "refs/tags") run: | - python3 -m pip install cibuildwheel==1.6.1 - python3 -m cibuildwheel --output-dir wheelhouse + python -m cibuildwheel --output-dir wheelhouse env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + - name: Upload linux wheels + uses: actions/upload-artifact@v2 + with: + name: OSX-wheels + path: wheelhouse/*.whl create_release: runs-on: ubuntu-latest needs: - build_linux - build_osx - if: startsWith(${{ github.ref }}, 'refs/tags') + if: startsWith(${{ github.ref }}, "refs/tags") steps: - name: Config deploy run: | From 3240ad1e16dc48dbed0f2c6013138536f6e6d008 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 14:41:06 +1100 Subject: [PATCH 0114/1681] Typo in workflow --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8d19ddb2..049926491 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: # https://docs.github.com/en/actions/guides/building-and-testing-python #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install tox tox-gh-actions cbuildwheel=1.6.1 + pip install tox tox-gh-actions cbuildwheel==1.6.1 - name: Test with tox run: tox - name: Wheels (linux) @@ -79,7 +79,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions cbuildwheels=1.6.1 + pip install tox tox-gh-actions cbuildwheels==1.6.1 - name: Test with tox run: tox - name: Wheels (OSX) From a5f574d435502f56f76313dd21ccdc84729c8cc6 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 14:42:49 +1100 Subject: [PATCH 0115/1681] Typo in workflow --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 049926491..9eb2498d5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: # https://docs.github.com/en/actions/guides/building-and-testing-python #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install tox tox-gh-actions cbuildwheel==1.6.1 + pip install tox tox-gh-actions cibuildwheel==1.6.1 - name: Test with tox run: tox - name: Wheels (linux) @@ -79,7 +79,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions cbuildwheels==1.6.1 + pip install tox tox-gh-actions cibuildwheels==1.6.1 - name: Test with tox run: tox - name: Wheels (OSX) From 5d0dd85b87d894a0c4234c3aa5f26b19e53f9b8c Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 14:45:11 +1100 Subject: [PATCH 0116/1681] Do not specify cibuildwheel version --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9eb2498d5..033f8a8d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: # https://docs.github.com/en/actions/guides/building-and-testing-python #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install tox tox-gh-actions cibuildwheel==1.6.1 + pip install tox tox-gh-actions cibuildwheel - name: Test with tox run: tox - name: Wheels (linux) @@ -79,7 +79,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions cibuildwheels==1.6.1 + pip install tox tox-gh-actions cibuildwheels - name: Test with tox run: tox - name: Wheels (OSX) From c4607d831b26524369715508a2543d74ec850826 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 15:05:41 +1100 Subject: [PATCH 0117/1681] Skip wheels for Python 2.7 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 033f8a8d1..762228b44 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,7 +42,7 @@ jobs: env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - CIBW_SKIP: "cp35-*" + CIBW_SKIP: "cp27-* pp27-* cp35-*" run: | python setup.py -q sdist python -m cibuildwheel --output-dir wheelhouse From 040a387dca283257c4100b329dd88e1b51294304 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 15:10:25 +1100 Subject: [PATCH 0118/1681] Separate jobs for wheels, which are in docker anyway --- .github/workflows/build.yml | 52 +++++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 762228b44..32b01b711 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,11 +34,35 @@ jobs: # https://docs.github.com/en/actions/guides/building-and-testing-python #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install tox tox-gh-actions cibuildwheel + pip install tox tox-gh-actions - name: Test with tox run: tox + + wheels_manylinux: + runs-on: ubuntu-latest + python-version: 3.8 + if: startsWith(${{ github.ref }}, "refs/tags") + needs: build_linux + steps: + - uses: actions/checkout@v1 + - name: Init C core submodule + run: git submodule update --init + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install OS dependencies + run: + sudo apt-get install gfortran flex bison + - name: Install dependencies + run: | + # Pypi has no pip by default, and ubuntu blocks python -m ensurepip + # However, Github runners are supposed to have pip installed by default + # https://docs.github.com/en/actions/guides/building-and-testing-python + #wget -qO- https://bootstrap.pypa.io/get-pip.py | python + python -m pip install --upgrade pip + pip install cibuildwheel - name: Wheels (linux) - if: startsWith(${{ github.ref }}, "refs/tags") env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" @@ -82,6 +106,24 @@ jobs: pip install tox tox-gh-actions cibuildwheels - name: Test with tox run: tox + + wheels_osx: + runs-on: macos-latest + python-version: 3.8 + if: startsWith(${{ github.ref }}, "refs/tags") + needs: build_osx + steps: + - uses: actions/checkout@v1 + - name: Init C core submodule + run: git submodule update --init + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install cibuildwheels - name: Wheels (OSX) if: startsWith(${{ github.ref }}, "refs/tags") run: | @@ -97,10 +139,10 @@ jobs: create_release: runs-on: ubuntu-latest - needs: - - build_linux - - build_osx if: startsWith(${{ github.ref }}, "refs/tags") + needs: + - wheels_manylinux + - wheels_osx steps: - name: Config deploy run: | From 33afec16dba3ad7c743929890c9060174b22b4de Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 15:11:35 +1100 Subject: [PATCH 0119/1681] Syntax in workflow --- .github/workflows/build.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 32b01b711..02f420efa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,7 +40,10 @@ jobs: wheels_manylinux: runs-on: ubuntu-latest - python-version: 3.8 + strategy: + matrix: + python-version: + - 3.8 if: startsWith(${{ github.ref }}, "refs/tags") needs: build_linux steps: @@ -109,7 +112,10 @@ jobs: wheels_osx: runs-on: macos-latest - python-version: 3.8 + strategy: + matrix: + python-version: + - 3.8 if: startsWith(${{ github.ref }}, "refs/tags") needs: build_osx steps: From 451c6b16f07e938847c70f5799f361df341e82fc Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 25 Jan 2021 15:17:00 +1100 Subject: [PATCH 0120/1681] No need for cibuildwheels during tox --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 02f420efa..ed51c9158 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,7 +106,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install tox tox-gh-actions cibuildwheels + pip install tox tox-gh-actions - name: Test with tox run: tox From 3cb54ef68fffc85ede372ac004a16067f634b9d8 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 26 Jan 2021 10:27:06 +1100 Subject: [PATCH 0121/1681] Split build and deploy workflows --- .github/workflows/build.yml | 113 +--------------------------------- .github/workflows/deploy.yml | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 112 deletions(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed51c9158..0c7d97584 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: "Build and deploy" +name: "Build and test with tox" on: - push @@ -38,53 +38,6 @@ jobs: - name: Test with tox run: tox - wheels_manylinux: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: - - 3.8 - if: startsWith(${{ github.ref }}, "refs/tags") - needs: build_linux - steps: - - uses: actions/checkout@v1 - - name: Init C core submodule - run: git submodule update --init - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install OS dependencies - run: - sudo apt-get install gfortran flex bison - - name: Install dependencies - run: | - # Pypi has no pip by default, and ubuntu blocks python -m ensurepip - # However, Github runners are supposed to have pip installed by default - # https://docs.github.com/en/actions/guides/building-and-testing-python - #wget -qO- https://bootstrap.pypa.io/get-pip.py | python - python -m pip install --upgrade pip - pip install cibuildwheel - - name: Wheels (linux) - env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - CIBW_SKIP: "cp27-* pp27-* cp35-*" - run: | - python setup.py -q sdist - python -m cibuildwheel --output-dir wheelhouse - - name: Upload source dist - uses: actions/upload-artifact@v2 - with: - name: source-dist - path: dist/python-igraph-*.tar.gz - - name: Upload linux wheels - uses: actions/upload-artifact@v2 - with: - name: linux-wheels - path: wheelhouse/*.whl - - build_osx: runs-on: macos-latest strategy: @@ -110,67 +63,3 @@ jobs: - name: Test with tox run: tox - wheels_osx: - runs-on: macos-latest - strategy: - matrix: - python-version: - - 3.8 - if: startsWith(${{ github.ref }}, "refs/tags") - needs: build_osx - steps: - - uses: actions/checkout@v1 - - name: Init C core submodule - run: git submodule update --init - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install cibuildwheels - - name: Wheels (OSX) - if: startsWith(${{ github.ref }}, "refs/tags") - run: | - python -m cibuildwheel --output-dir wheelhouse - env: - CIBW_BEFORE_BUILD: "python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - - name: Upload linux wheels - uses: actions/upload-artifact@v2 - with: - name: OSX-wheels - path: wheelhouse/*.whl - - create_release: - runs-on: ubuntu-latest - if: startsWith(${{ github.ref }}, "refs/tags") - needs: - - wheels_manylinux - - wheels_osx - steps: - - name: Config deploy - run: | - git config --local user.name "ntamas" - git config --local user.email "ntamas@gmail.com" - - - name: Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: true - prerelease: false - - - name: Upload binaries and source - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ github.token }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' - diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..521d11ed1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,115 @@ +name: "Build wheels and deploy" + +on: + push: + tags: + - '*.*.*' + +jobs: + wheels_manylinux: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - 3.8 + needs: build_linux + steps: + - uses: actions/checkout@v1 + - name: Init C core submodule + run: git submodule update --init + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install OS dependencies + run: + sudo apt-get install gfortran flex bison + - name: Install dependencies + run: | + # Pypi has no pip by default, and ubuntu blocks python -m ensurepip + # However, Github runners are supposed to have pip installed by default + # https://docs.github.com/en/actions/guides/building-and-testing-python + #wget -qO- https://bootstrap.pypa.io/get-pip.py | python + python -m pip install --upgrade pip + pip install cibuildwheel + - name: Wheels (linux) + env: + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_SKIP: "cp27-* pp27-* cp35-*" + run: | + python setup.py -q sdist + python -m cibuildwheel --output-dir wheelhouse + - name: Upload source dist + uses: actions/upload-artifact@v2 + with: + name: source-dist + path: dist/python-igraph-*.tar.gz + - name: Upload linux wheels + uses: actions/upload-artifact@v2 + with: + name: linux-wheels + path: wheelhouse/*.whl + + wheels_osx: + runs-on: macos-latest + strategy: + matrix: + python-version: + - 3.8 + needs: build_osx + steps: + - uses: actions/checkout@v1 + - name: Init C core submodule + run: git submodule update --init + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install cibuildwheels + - name: Wheels (OSX) + if: startsWith(${{ github.ref }}, "refs/tags") + run: | + python -m cibuildwheel --output-dir wheelhouse + env: + CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + - name: Upload linux wheels + uses: actions/upload-artifact@v2 + with: + name: OSX-wheels + path: wheelhouse/*.whl + + create_release: + runs-on: ubuntu-latest + needs: + - wheels_manylinux + - wheels_osx + steps: + - name: Config deploy + run: | + git config --local user.name "ntamas" + git config --local user.email "ntamas@gmail.com" + + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: true + prerelease: false + + - name: Upload binaries and source + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ github.token }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' + From 92550337bfd1148842127d91662e02a208654fa4 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 11:54:12 +1100 Subject: [PATCH 0122/1681] Brew autotools on OSX --- .github/workflows/build.yml | 7 +++++-- .github/workflows/deploy.yml | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0c7d97584..30a219771 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,7 +27,7 @@ jobs: - name: Install OS dependencies run: sudo apt-get install gfortran flex bison - - name: Install dependencies + - name: Install Python dependencies run: | # Pypi has no pip by default, and ubuntu blocks python -m ensurepip # However, Github runners are supposed to have pip installed by default @@ -56,7 +56,10 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install OS dependencies + run: + brew install autoconf automake libtool + - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install tox tox-gh-actions diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 521d11ed1..b1ca0c641 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,7 @@ jobs: - name: Install OS dependencies run: sudo apt-get install gfortran flex bison - - name: Install dependencies + - name: Install Python dependencies run: | # Pypi has no pip by default, and ubuntu blocks python -m ensurepip # However, Github runners are supposed to have pip installed by default @@ -66,7 +66,10 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install OS dependencies + run: + brew install autoconf automake libtool + - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install cibuildwheels From aae35eb0cb330181fbffa120b488eabc76d5d669 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 12:25:33 +1100 Subject: [PATCH 0123/1681] Test deploy workflow --- .github/workflows/deploy.yml | 45 ++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b1ca0c641..26fa1c81c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,9 +1,11 @@ name: "Build wheels and deploy" on: - push: - tags: - - '*.*.*' + #FIXME: for testing, remove comments + - push + #push: + # tags: + # - '*.*.*' jobs: wheels_manylinux: @@ -108,11 +110,36 @@ jobs: draft: true prerelease: false - - name: Upload binaries and source - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ github.token }} + - name: Download artifacts: source dist + uses: actions/download-artifact@v2 + with: + name: source-dist + path: dist + + - name: Download artifacts: linux wheels + uses: actions/download-artifact@v2 with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' + name: linux-wheels + path: wheelhouse + + - name: Download artifacts: OSX wheels + uses: actions/download-artifact@v2 + with: + name: OSX-wheels + path: wheelhouse + + - name: List contents of artifact folders + run: | + echo "dist folder:" + ls -lh dist + echo "wheelhouse folder:" + ls -lh wheelhouse + + #- name: Upload binaries and source + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ github.token }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' From 2da38d3a0a72c70ab8ecf99523b23fc2cd19d654 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 12:26:41 +1100 Subject: [PATCH 0124/1681] Syntax in deploy yml --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 26fa1c81c..2a3b16820 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -110,25 +110,25 @@ jobs: draft: true prerelease: false - - name: Download artifacts: source dist + - name: "Download artifacts: source dist" uses: actions/download-artifact@v2 with: name: source-dist path: dist - - name: Download artifacts: linux wheels + - name: "Download artifacts: linux wheels" uses: actions/download-artifact@v2 with: name: linux-wheels path: wheelhouse - - name: Download artifacts: OSX wheels + - name: "Download artifacts: OSX wheels" uses: actions/download-artifact@v2 with: name: OSX-wheels path: wheelhouse - - name: List contents of artifact folders + - name: "List contents of artifact folders" run: | echo "dist folder:" ls -lh dist From 9449beb285dc115ced480ee10663e873365531e7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 12:27:19 +1100 Subject: [PATCH 0125/1681] Syntax in deploy yml, split in two --- .github/workflows/deploy.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2a3b16820..baa661f39 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -14,7 +14,6 @@ jobs: matrix: python-version: - 3.8 - needs: build_linux steps: - uses: actions/checkout@v1 - name: Init C core submodule @@ -59,7 +58,6 @@ jobs: matrix: python-version: - 3.8 - needs: build_osx steps: - uses: actions/checkout@v1 - name: Init C core submodule From c4de217410c51fa381376f77540106d7a585a55f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 13:16:57 +1100 Subject: [PATCH 0126/1681] Typo in cibuildwheel --- .github/workflows/deploy.yml | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index baa661f39..74d7de0f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,18 +10,12 @@ on: jobs: wheels_manylinux: runs-on: ubuntu-latest - strategy: - matrix: - python-version: - - 3.8 steps: - uses: actions/checkout@v1 - name: Init C core submodule run: git submodule update --init - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - name: Install OS dependencies run: sudo apt-get install gfortran flex bison @@ -54,27 +48,20 @@ jobs: wheels_osx: runs-on: macos-latest - strategy: - matrix: - python-version: - - 3.8 steps: - uses: actions/checkout@v1 - name: Init C core submodule run: git submodule update --init - - name: Set up Python ${{ matrix.python-version }} + - name: Set up Python uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - name: Install OS dependencies run: brew install autoconf automake libtool - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install cibuildwheels + pip install cibuildwheel - name: Wheels (OSX) - if: startsWith(${{ github.ref }}, "refs/tags") run: | python -m cibuildwheel --output-dir wheelhouse env: @@ -133,11 +120,10 @@ jobs: echo "wheelhouse folder:" ls -lh wheelhouse - #- name: Upload binaries and source - # uses: actions/upload-release-asset@v1 - # env: - # GITHUB_TOKEN: ${{ github.token }} - # with: - # upload_url: ${{ steps.create_release.outputs.upload_url }} - # asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' + #- name: Upload binaries and source + # uses: alexellis/upload-assets@0.2.2 + # env: + # GITHUB_TOKEN: ${{ github.token }} + # with: + # asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' From 7fba20954d00accfacad3ab2fd5825a7ee299ba2 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 13:23:06 +1100 Subject: [PATCH 0127/1681] Skip python 2.7 on mac wheels --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 74d7de0f4..5dbe95f95 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -67,6 +67,7 @@ jobs: env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_SKIP: "cp27-* pp27-* cp35-*" - name: Upload linux wheels uses: actions/upload-artifact@v2 with: From d9cfb3ef8029b40b7c35e141a5c922817c0a1691 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 14:02:27 +1100 Subject: [PATCH 0128/1681] Remove git config, unnecessary --- .github/workflows/deploy.yml | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5dbe95f95..b82aa755b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -80,22 +80,6 @@ jobs: - wheels_manylinux - wheels_osx steps: - - name: Config deploy - run: | - git config --local user.name "ntamas" - git config --local user.email "ntamas@gmail.com" - - - name: Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} - draft: true - prerelease: false - - name: "Download artifacts: source dist" uses: actions/download-artifact@v2 with: @@ -121,6 +105,17 @@ jobs: echo "wheelhouse folder:" ls -lh wheelhouse + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: true + prerelease: false + #- name: Upload binaries and source # uses: alexellis/upload-assets@0.2.2 # env: From 8f1c6456bfd9bfa47a453757bcf75354553b027c Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 18:35:00 +1100 Subject: [PATCH 0129/1681] Restore tag workflow to tags --- .github/workflows/deploy.yml | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b82aa755b..749349465 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,11 +1,9 @@ name: "Build wheels and deploy" on: - #FIXME: for testing, remove comments - - push - #push: - # tags: - # - '*.*.*' + push: + tags: + - '*.*.*' jobs: wheels_manylinux: @@ -116,10 +114,10 @@ jobs: draft: true prerelease: false - #- name: Upload binaries and source - # uses: alexellis/upload-assets@0.2.2 - # env: - # GITHUB_TOKEN: ${{ github.token }} - # with: - # asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' + - name: Upload binaries and source + uses: alexellis/upload-assets@0.2.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' From adb252ad2a4023f78362a6bf57626d5596dcac1f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 18:35:48 +1100 Subject: [PATCH 0130/1681] Remove travis.yml --- .travis.yml | 80 ----------------------------------------------------- 1 file changed, 80 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8265b24c8..000000000 --- a/.travis.yml +++ /dev/null @@ -1,80 +0,0 @@ -dist: bionic -language: python - -python: - - "3.6" - - "3.7" - - "3.8" - - "3.9-dev" - - pypy3 - -addons: - apt: - packages: - - gfortran - - flex - - bison - -install: - - python -m pip install tox-travis - -script: - - tox - -jobs: - include: - - stage: test - services: docker - - - stage: wheels - services: docker - language: shell - env: - - CIBW_BEFORE_BUILD="yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" - - CIBW_TEST_COMMAND="cd {project} && python -m unittest" - - CIBW_SKIP="cp35-*" - install: - - wget -qO- https://bootstrap.pypa.io/get-pip.py | sudo python3 - - sudo python3 -m pip install cibuildwheel==1.6.1 - script: - - python setup.py -q sdist - - python3 -m cibuildwheel --output-dir wheelhouse - before_deploy: &before_deploy_releases - - git config --local user.name "ntamas" - - git config --local user.email "ntamas@gmail.com" - # - export TRAVIS_TAG=0.8.0 - # - git tag $TRAVIS_TAG - deploy: &deploy_releases - provider: releases - token: - secure: CAn8qENTIZSFed7VjqcQR42mjrPGV+QptW2XkncPaRwFooizPKgRsphgKzgv7k7peEQhQs/WOEtL0a3ofYRbbucOVbJ/nTVI0qGba9Lz/afsQ2UsbGnan0hXua9D/bo1wRhgqz8j0q6LHb3O8rgQOAKi8hWCChglr2saeRsy5Fc= - file: - - wheelhouse/*.whl - - dist/python-igraph-${TRAVIS_TAG}.tar.gz - file_glob: true - draft: true - edge: true - overwrite: true - cleanup: false - on: - repo: igraph/python-igraph - tags: true - - - stage: wheels - os: osx - osx_image: xcode11.2 - language: shell - env: - - CIBW_BEFORE_BUILD="python setup.py build_c_core" - - CIBW_TEST_COMMAND="cd {project} && python -m unittest" - install: - - python3 -m pip install cibuildwheel==1.6.1 - script: - - python3 -m cibuildwheel --output-dir wheelhouse - before_deploy: *before_deploy_releases - deploy: *deploy_releases - -notifications: - email: - on_success: change - on_failure: always From 08d73e424518d7099f4992e1bf27318dc758db8c Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 19:50:55 +1100 Subject: [PATCH 0131/1681] Try different action for deploying --- .github/workflows/deploy.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 749349465..51a394fc8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -115,9 +115,7 @@ jobs: prerelease: false - name: Upload binaries and source - uses: alexellis/upload-assets@0.2.2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + uses: AButler/upload-release-assets@v2.0 with: - asset_paths: '["wheelhouse/*.whl", "dist/python-igraph-*.tar.gz"]' - + files: 'wheelhouse/*.whl;dist/python-igraph-*.tar.gz' + repo-token: ${{ secrets.GITHUB_TOKEN }} From db0a8db181041ee49efc85d6a6ab6a39b8e00dec Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 19:53:46 +1100 Subject: [PATCH 0132/1681] Ignore build on tags --- .github/workflows/build.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30a219771..7e54e0d98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,14 @@ name: "Build and test with tox" on: - - push - - pull_request + push: + tags-ignore: + - '*.*.*' + pull_request: + tags-ignore: + - '*.*.*' + + jobs: build_linux: From 666e83b8e6b31635fdab8335d1d46ccaf313c27e Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 27 Jan 2021 19:55:52 +1100 Subject: [PATCH 0133/1681] Syntax to ignore tags --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7e54e0d98..db8c52171 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,13 @@ name: "Build and test with tox" on: push: + branches: + - '*' tags-ignore: - '*.*.*' pull_request: + branches: + - '*' tags-ignore: - '*.*.*' From 81b62499e11f718eafa391762fa56a7130ae6f9b Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 28 Jan 2021 08:33:16 +1100 Subject: [PATCH 0134/1681] Add manual trigger for deploy --- .github/workflows/deploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 51a394fc8..08f44b33f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,6 +5,8 @@ on: tags: - '*.*.*' + workflow_dispatch: + jobs: wheels_manylinux: runs-on: ubuntu-latest From 85dfd295e3b21049e1bc60dc637f67536d3518e0 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 28 Jan 2021 08:36:26 +1100 Subject: [PATCH 0135/1681] Test syntax for manual dispatch --- .github/workflows/deploy.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 08f44b33f..6b93f8232 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,6 +6,9 @@ on: - '*.*.*' workflow_dispatch: + inputs: + tags: + description: 'Test wheels and deploy' jobs: wheels_manylinux: From 4e4249b3f7874dabcb20b413e02e5a0304774efe Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 28 Jan 2021 08:46:24 +1100 Subject: [PATCH 0136/1681] Add repository dispatch --- .github/workflows/deploy.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6b93f8232..6d954366e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,10 +5,8 @@ on: tags: - '*.*.*' + repository_dispatch: workflow_dispatch: - inputs: - tags: - description: 'Test wheels and deploy' jobs: wheels_manylinux: From 679ef11a6228b241456fe4219f062965edff7189 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 28 Jan 2021 09:18:52 +1100 Subject: [PATCH 0137/1681] Remore repo dispatch and add instructions --- .github/workflows/deploy.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6d954366e..081f8d9c2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,11 +1,14 @@ name: "Build wheels and deploy" +# To trigger manually, make yourself an auth token including "workflow" scope and run: +# curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token " https://api.github.com/repos/igraph/python-igraph/actions/workflows/deploy.yml/dispatches -d '{"ref":"master"}' +# If you want to release from another branch and not "master", change the last string. + on: push: tags: - '*.*.*' - repository_dispatch: workflow_dispatch: jobs: From fb0370360a7ea5fa2a437c5c3fe985ffe7c05a3a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 28 Jan 2021 16:25:17 +1100 Subject: [PATCH 0138/1681] Some bash magic to overcome manual triggering --- .github/workflows/deploy.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 081f8d9c2..218fe07ab 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,11 +5,11 @@ name: "Build wheels and deploy" # If you want to release from another branch and not "master", change the last string. on: + workflow_dispatch: push: tags: - '*.*.*' - workflow_dispatch: jobs: wheels_manylinux: @@ -109,14 +109,19 @@ jobs: echo "wheelhouse folder:" ls -lh wheelhouse + - name: Figure out version + id: get_version + run: | + echo "igraph_version=$(fn=$(ls dist); fn=${fn##*-}; echo ${fn%.tar.gz})" >> GITHUB_ENV + - name: Create release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token with: - tag_name: ${{ github.ref }} - release_name: Release ${{ github.ref }} + tag_name: ${{ env.igraph_version }} + release_name: Release ${{ env.igraph_version }} draft: true prerelease: false @@ -125,3 +130,4 @@ jobs: with: files: 'wheelhouse/*.whl;dist/python-igraph-*.tar.gz' repo-token: ${{ secrets.GITHUB_TOKEN }} + release-tag: ${{ env.igraph_version }} From d84cfc50273f276bcbfa7c29a67da1b25d4329fe Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 28 Jan 2021 22:18:09 +1100 Subject: [PATCH 0139/1681] Check env variable --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 218fe07ab..f16f684ac 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -113,6 +113,7 @@ jobs: id: get_version run: | echo "igraph_version=$(fn=$(ls dist); fn=${fn##*-}; echo ${fn%.tar.gz})" >> GITHUB_ENV + tail -n 1 $GITHUB_ENV - name: Create release id: create_release From 01264d1ac58533f500502623a4458450734c7aa4 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 29 Jan 2021 08:15:48 +1100 Subject: [PATCH 0140/1681] Better title for step --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f16f684ac..76ceaa136 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -109,7 +109,7 @@ jobs: echo "wheelhouse folder:" ls -lh wheelhouse - - name: Figure out version + - name: Store igraph version id: get_version run: | echo "igraph_version=$(fn=$(ls dist); fn=${fn##*-}; echo ${fn%.tar.gz})" >> GITHUB_ENV From f2c44e3b6fdf7f0ff99cbdab0b78cc6abf80f1a7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 29 Jan 2021 17:02:55 +1100 Subject: [PATCH 0141/1681] Forgot $ in bash variable --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 76ceaa136..75f7d5264 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -112,7 +112,7 @@ jobs: - name: Store igraph version id: get_version run: | - echo "igraph_version=$(fn=$(ls dist); fn=${fn##*-}; echo ${fn%.tar.gz})" >> GITHUB_ENV + echo "igraph_version=$(fn=$(ls dist); fn=${fn##*-}; echo ${fn%.tar.gz})" >> $GITHUB_ENV tail -n 1 $GITHUB_ENV - name: Create release From 673d139a813ed2ea8d8e1cacffb392b5e46a1725 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 29 Jan 2021 18:40:09 +1100 Subject: [PATCH 0142/1681] Make upload assets for loops by hand --- .github/workflows/deploy.yml | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 75f7d5264..3812df96f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -126,9 +126,33 @@ jobs: draft: true prerelease: false + #- name: Upload binaries and source + # uses: AButler/upload-release-assets@v2.0 + # with: + # files: 'wheelhouse/*.whl;dist/python-igraph-*.tar.gz' + # repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Move artifacts into one folder + run: | + mkdir upload + mv dist/* upload/ + mv wheelhouse/* upload/ + ls upload + - name: Upload binaries and source - uses: AButler/upload-release-assets@v2.0 + uses: actions/github-script@v3 with: - files: 'wheelhouse/*.whl;dist/python-igraph-*.tar.gz' - repo-token: ${{ secrets.GITHUB_TOKEN }} - release-tag: ${{ env.igraph_version }} + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const path = require('path'); + const fs = require('fs'); + const release_id = '${{ needs.create_release.outputs.id }}'; + for (let file of await fs.readdirSync('./upload/')) { + console.log('uploadReleaseAsset', file); + await github.repos.uploadReleaseAsset({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: release_id, + name: file, + data: await fs.readFileSync(`./${file}`) + }); + } From 37e05108208994592c3e65c4bb21b352bdc586f3 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 29 Jan 2021 19:54:28 +1100 Subject: [PATCH 0143/1681] Syntax of github script --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3812df96f..3d04d14e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -153,6 +153,6 @@ jobs: repo: context.repo.repo, release_id: release_id, name: file, - data: await fs.readFileSync(`./${file}`) + data: await fs.readFileSync(`./upload/${file}`) }); } From f39b03855ace3cdb646779f77ee3f17362a92659 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 29 Jan 2021 21:08:19 +1100 Subject: [PATCH 0144/1681] Syntax in github script --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3d04d14e8..4ee06b956 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -145,7 +145,7 @@ jobs: script: | const path = require('path'); const fs = require('fs'); - const release_id = '${{ needs.create_release.outputs.id }}'; + const release_id = '${{ steps.create_release.outputs.id }}'; for (let file of await fs.readdirSync('./upload/')) { console.log('uploadReleaseAsset', file); await github.repos.uploadReleaseAsset({ From f02f420686bdb6f0e4386a07294cbdd257c35d3a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 20 Jan 2021 10:18:35 +1100 Subject: [PATCH 0145/1681] Generate graph from scipy sparse matrix --- src/igraph/__init__.py | 45 +++++++++++++++ src/igraph/sparse_matrix.py | 109 ++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/igraph/sparse_matrix.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 6f05e44ac..0f2c2fb41 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -41,6 +41,7 @@ from igraph.layout import * from igraph.matching import * from igraph.operators import * +from igraph.sparse_matrix import * from igraph.statistics import * from igraph.summary import * from igraph.utils import * @@ -2105,6 +2106,50 @@ def Read_Adjacency( return graph + @classmethod + def Adjacency(klass, matrix, mode=ADJ_DIRECTED): + """Generates a graph from its adjacency matrix. + + @param matrix: the adjacency matrix. Possible types are: + - a list of lists + - a numpy 2D array or matrix (will be converted to list of lists) + - a scipy.sparse matrix (will be converted to a COO matrix, but not + to a dense matrix) + @param mode: the mode to be used. Possible values are: + + - C{ADJ_DIRECTED} - the graph will be directed and a matrix + element gives the number of edges between two vertex. + - C{ADJ_UNDIRECTED} - alias to C{ADJ_MAX} for convenience. + - C{ADJ_MAX} - undirected graph will be created and the number of + edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{ADJ_MIN} - like C{ADJ_MAX}, but with M{min(A(i,j), A(j,i))} + - C{ADJ_PLUS} - like C{ADJ_MAX}, but with M{A(i,j) + A(j,i)} + - C{ADJ_UPPER} - undirected graph with the upper right triangle of + the matrix (including the diagonal) + - C{ADJ_LOWER} - undirected graph with the lower left triangle of + the matrix (including the diagonal) + + These values can also be given as strings without the C{ADJ} prefix. + + """ + try: + import numpy as np + except ImportError: + np = None + + try: + from scipy import sparse + except ImportError: + sparse = None + + if (sparse is not None) and isinstance(matrix, sparse.spmatrix): + return _graph_from_sparse_matrix(klass, matrix, mode=mode) + + if (np is not None) and isinstance(matrix, np.ndarray): + matrix = matrix.tolist() + + return GraphBase.Adjacency(klass, matrix, mode=mode) + def write_dimacs(self, f, source=None, target=None, capacity="capacity"): """Writes the graph in DIMACS format to the given file. diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py new file mode 100644 index 000000000..dc86d0a37 --- /dev/null +++ b/src/igraph/sparse_matrix.py @@ -0,0 +1,109 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +"""Implementation of union, disjoint union and intersection operators.""" + +from __future__ import with_statement + +__all__ = () +__docformat__ = "restructuredtext en" +__license__ = u""" +Copyright (C) 2021 igraph development team + +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 2 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, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA +""" + +# pylint: disable-msg=W0401 +# W0401: wildcard import +from igraph._igraph import * + + +# Logic to get graph from scipy sparse matrix. This would be simple if there +# weren't so many modes. +def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): + # This function assumes there is scipy and the matrix is a scipy sparse + # matrix. The caller should make sure those conditions are met. + from scipy import sparse + + modes = ( + ADJ_DIRECTED, + ADJ_UNDIRECTED, + ADJ_MAX, + ADJ_MIN, + ADJ_PLUS, + ADJ_UPPER, + ADJ_LOWER, + ) + + if not isinstance(matrix, sparse.coo_matrix): + matrix = matrix.tocoo() + + # Shorthand notation + m = matrix + + if mode == ADJ_DIRECTED: + edges = sum( + ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data)), + [], + ) + return klass( + edges=edges, directed=True, + ) + + if mode == ADJ_UNDIRECTED: + mode = ADJ_MAX + + if mode in (ADJ_MAX, ADJ_MIN, ADJ_PLUS): + fun_dict = { + ADJ_MAX: max, + ADJ_MIN: min, + ADJ_PLUS: lambda *x: sum(x), + } + fun = fun_dict[mode] + nedges = {} + for i, j, n in zip(m.row, m.col, m.data): + # Fist time this pair of vertices + if (j, i) not in nedges: + nedges[(i, j)] = n + else: + nedges[(j, i)] = fun(nedges[(j, i)], n) + + edges = sum( + ([e] * n for e, n in nedges.items()), + [], + ) + return klass( + edges=edges, directed=False, + ) + + if mode == ADJ_UPPER: + edges = sum( + ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j >= i), + [], + ) + return klass( + edges=edges, directed=True, + ) + + if mode == ADJ_LOWER: + edges = sum( + ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j <= i), + [], + ) + return klass( + edges=edges, directed=True, + ) + + raise ValueError('mode should be one of ' + ' '.join(map(str, modes))) From 2680e61fe228726462fa88a99bcb9783417cdd8d Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 20 Jan 2021 11:18:57 +1100 Subject: [PATCH 0146/1681] Hack via _Adjacency --- src/_igraph/graphobject.c | 6 ++++++ src/_igraph/graphobject.h | 1 + src/igraph/__init__.py | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bab3391ac..9787e455b 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1939,6 +1939,12 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, return (PyObject *) self; } +PyObject *igraphmodule_Graph__Adjacency(PyTypeObject * type, + PyObject * args, PyObject * kwds) { + return igraphmodule_Graph_Adjacency(type, args, kwds); +} + + /** \ingroup python_interface_graph * \brief Generates a graph from the Graph Atlas * \return a reference to the newly generated Python igraph object diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 618dbb97b..6a650251b 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -77,6 +77,7 @@ PyObject* igraphmodule_Graph_predecessors(igraphmodule_GraphObject *self, PyObje PyObject* igraphmodule_Graph_get_eid(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Adjacency(PyTypeObject *type, PyObject *args, PyObject *kwds); +PyObject* igraphmodule_Graph__Adjacency(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Asymmetric_Preference(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Atlas(PyTypeObject *type, PyObject *args); PyObject* igraphmodule_Graph_Barabasi(PyTypeObject *type, PyObject *args, PyObject *kwds); diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 0f2c2fb41..0140f2d58 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2107,7 +2107,7 @@ def Read_Adjacency( return graph @classmethod - def Adjacency(klass, matrix, mode=ADJ_DIRECTED): + def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): """Generates a graph from its adjacency matrix. @param matrix: the adjacency matrix. Possible types are: @@ -2148,7 +2148,7 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED): if (np is not None) and isinstance(matrix, np.ndarray): matrix = matrix.tolist() - return GraphBase.Adjacency(klass, matrix, mode=mode) + return klass._Adjacency(matrix, mode=mode) def write_dimacs(self, f, source=None, target=None, capacity="capacity"): """Writes the graph in DIMACS format to the given file. From 44487291d3867fc9af1d7fbacc792c9372182e0b Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 29 Jan 2021 17:01:25 +1100 Subject: [PATCH 0147/1681] Use super() as suggested by everyone - sigh! --- src/_igraph/graphobject.c | 5 ----- src/_igraph/graphobject.h | 1 - src/igraph/__init__.py | 2 +- src/igraph/sparse_matrix.py | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 9787e455b..87cb145a8 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1939,11 +1939,6 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, return (PyObject *) self; } -PyObject *igraphmodule_Graph__Adjacency(PyTypeObject * type, - PyObject * args, PyObject * kwds) { - return igraphmodule_Graph_Adjacency(type, args, kwds); -} - /** \ingroup python_interface_graph * \brief Generates a graph from the Graph Atlas diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 6a650251b..618dbb97b 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -77,7 +77,6 @@ PyObject* igraphmodule_Graph_predecessors(igraphmodule_GraphObject *self, PyObje PyObject* igraphmodule_Graph_get_eid(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Adjacency(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph__Adjacency(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Asymmetric_Preference(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Atlas(PyTypeObject *type, PyObject *args); PyObject* igraphmodule_Graph_Barabasi(PyTypeObject *type, PyObject *args, PyObject *kwds); diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 0140f2d58..c735abcec 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2148,7 +2148,7 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): if (np is not None) and isinstance(matrix, np.ndarray): matrix = matrix.tolist() - return klass._Adjacency(matrix, mode=mode) + return super().Adjacency(matrix, mode=mode) def write_dimacs(self, f, source=None, target=None, capacity="capacity"): """Writes the graph in DIMACS format to the given file. diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index dc86d0a37..0c4daf398 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -4,7 +4,7 @@ from __future__ import with_statement -__all__ = () +__all__ = ['_graph_from_sparse_matrix'] __docformat__ = "restructuredtext en" __license__ = u""" Copyright (C) 2021 igraph development team From 63b7ba11d5776c09c78a0685dcb7dbba3431b156 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 30 Jan 2021 10:25:02 +1100 Subject: [PATCH 0148/1681] Weighted matrix functions --- src/igraph/__init__.py | 57 ++++++++++++- src/igraph/sparse_matrix.py | 160 ++++++++++++++++++++++++++---------- 2 files changed, 174 insertions(+), 43 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index c735abcec..e704de67d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -41,11 +41,14 @@ from igraph.layout import * from igraph.matching import * from igraph.operators import * -from igraph.sparse_matrix import * from igraph.statistics import * from igraph.summary import * from igraph.utils import * from igraph.version import __version__, __version_info__ +from igraph.sparse_matrix import ( + _graph_from_sparse_matrix, + _graph_from_weighted_sparse_matrix, + ) import os import math @@ -2150,6 +2153,58 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): return super().Adjacency(matrix, mode=mode) + @classmethod + def WeightedAdjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=True): + """Generates a graph from its weighted adjacency matrix. + + @param matrix: the adjacency matrix. Possible types are: + - a list of lists + - a numpy 2D array or matrix (will be converted to list of lists) + - a scipy.sparse matrix (will be converted to a COO matrix, but not + to a dense matrix) + @param mode: the mode to be used. Possible values are: + + - C{ADJ_DIRECTED} - the graph will be directed and a matrix + element gives the number of edges between two vertex. + - C{ADJ_UNDIRECTED} - alias to C{ADJ_MAX} for convenience. + - C{ADJ_MAX} - undirected graph will be created and the number of + edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{ADJ_MIN} - like C{ADJ_MAX}, but with M{min(A(i,j), A(j,i))} + - C{ADJ_PLUS} - like C{ADJ_MAX}, but with M{A(i,j) + A(j,i)} + - C{ADJ_UPPER} - undirected graph with the upper right triangle of + the matrix (including the diagonal) + - C{ADJ_LOWER} - undirected graph with the lower left triangle of + the matrix (including the diagonal) + + These values can also be given as strings without the C{ADJ} prefix. + @param attr: the name of the edge attribute that stores the edge + weights. + @param loops: whether to include loop edges. When C{False}, the diagonal + of the adjacency matrix will be ignored. + + """ + try: + import numpy as np + except ImportError: + np = None + + try: + from scipy import sparse + except ImportError: + sparse = None + + if (sparse is not None) and isinstance(matrix, sparse.spmatrix): + return _graph_from_weighted_sparse_matrix( + klass, matrix, mode=mode, attr=attr, + ) + + if (np is not None) and isinstance(matrix, np.ndarray): + matrix = matrix.tolist() + + return super().WeightedAdjacency( + matrix, mode=mode, attr=attr, loops=loops, + ) + def write_dimacs(self, f, source=None, target=None, capacity="capacity"): """Writes the graph in DIMACS format to the given file. diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index 0c4daf398..101a0c10f 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -1,10 +1,10 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- -"""Implementation of union, disjoint union and intersection operators.""" +"""Implementation of Python-level sparse matrix operations.""" from __future__ import with_statement -__all__ = ['_graph_from_sparse_matrix'] +__all__ = () __docformat__ = "restructuredtext en" __license__ = u""" Copyright (C) 2021 igraph development team @@ -25,14 +25,22 @@ 02110-1301 USA """ -# pylint: disable-msg=W0401 -# W0401: wildcard import -from igraph._igraph import * +from operator import add +from igraph._igraph import ( + ADJ_DIRECTED, + ADJ_UNDIRECTED, + ADJ_MAX, + ADJ_MIN, + ADJ_PLUS, + ADJ_UPPER, + ADJ_LOWER, +) # Logic to get graph from scipy sparse matrix. This would be simple if there # weren't so many modes. def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): + '''Construct graph from sparse matrix, unweighted''' # This function assumes there is scipy and the matrix is a scipy sparse # matrix. The caller should make sure those conditions are met. from scipy import sparse @@ -53,23 +61,20 @@ def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): # Shorthand notation m = matrix - if mode == ADJ_DIRECTED: - edges = sum( - ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data)), - [], - ) - return klass( - edges=edges, directed=True, - ) - if mode == ADJ_UNDIRECTED: mode = ADJ_MAX - if mode in (ADJ_MAX, ADJ_MIN, ADJ_PLUS): + if mode == ADJ_DIRECTED: + edges = sum( + ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data)), + [], + ) + + elif mode in (ADJ_MAX, ADJ_MIN, ADJ_PLUS): fun_dict = { - ADJ_MAX: max, - ADJ_MIN: min, - ADJ_PLUS: lambda *x: sum(x), + ADJ_MAX: max, + ADJ_MIN: min, + ADJ_PLUS: add, } fun = fun_dict[mode] nedges = {} @@ -81,29 +86,100 @@ def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): nedges[(j, i)] = fun(nedges[(j, i)], n) edges = sum( - ([e] * n for e, n in nedges.items()), - [], - ) - return klass( - edges=edges, directed=False, - ) - - if mode == ADJ_UPPER: + ([e] * n for e, n in nedges.items()), + [], + ) + + elif mode == ADJ_UPPER: edges = sum( - ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j >= i), - [], - ) - return klass( - edges=edges, directed=True, - ) - - if mode == ADJ_LOWER: + ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j >= i), + [], + ) + + elif mode == ADJ_LOWER: edges = sum( - ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j <= i), - [], - ) - return klass( - edges=edges, directed=True, - ) - - raise ValueError('mode should be one of ' + ' '.join(map(str, modes))) + ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j <= i), + [], + ) + + else: + raise ValueError("mode should be one of " + " ".join(map(str, modes))) + + return klass( + edges=edges, + directed=mode == ADJ_DIRECTED, + ) + + +def _graph_from_weighted_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED, attr='weight'): + '''Construct graph from sparse matrix, weighted + + NOTE: Of course, you cannot emcompass a fully general weighted multigraph + with a single adjacency matrix, so we don't try to do it here either. + ''' + # This function assumes there is scipy and the matrix is a scipy sparse + # matrix. The caller should make sure those conditions are met. + from scipy import sparse + + modes = ( + ADJ_DIRECTED, + ADJ_UNDIRECTED, + ADJ_MAX, + ADJ_MIN, + ADJ_PLUS, + ADJ_UPPER, + ADJ_LOWER, + ) + + if not isinstance(matrix, sparse.coo_matrix): + matrix = matrix.tocoo() + + # Shorthand notation + m = matrix + + if mode == ADJ_UNDIRECTED: + mode = ADJ_MAX + + if mode == ADJ_DIRECTED: + edges = list(zip(m.row, m.col)) + weights = list(m.data) + + elif mode in (ADJ_MAX, ADJ_MIN, ADJ_PLUS): + fun_dict = { + ADJ_MAX: max, + ADJ_MIN: min, + ADJ_PLUS: add, + } + fun = fun_dict[mode] + nedges = {} + for i, j, n in zip(m.row, m.col, m.data): + # Fist time this pair of vertices + if (j, i) not in nedges: + nedges[(i, j)] = n + else: + nedges[(j, i)] = fun(nedges[(j, i)], n) + + edges, weights = zip(*nedges.items()) + + elif mode == ADJ_UPPER: + edges, weights = [], [] + for i, j, n in zip(m.row, m.col, m.data): + if j >= i: + edges.append((i, j)) + weights.append(n) + + elif mode == ADJ_LOWER: + edges, weights = [], [] + for i, j, n in zip(m.row, m.col, m.data): + if j <= i: + edges.append((i, j)) + weights.append(n) + + else: + raise ValueError("mode should be one of " + " ".join(map(str, modes))) + + return klass( + edges=edges, + directed=mode == ADJ_DIRECTED, + edge_attrs={attr: weights}, + ) From 99d0bb5daf2c79e11f52e4e2a594a6e917a29a03 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 30 Jan 2021 10:30:26 +1100 Subject: [PATCH 0149/1681] Black lint --- src/igraph/__init__.py | 16 +++++++++++----- src/igraph/sparse_matrix.py | 8 ++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index e704de67d..cedecac04 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -46,9 +46,9 @@ from igraph.utils import * from igraph.version import __version__, __version_info__ from igraph.sparse_matrix import ( - _graph_from_sparse_matrix, - _graph_from_weighted_sparse_matrix, - ) + _graph_from_sparse_matrix, + _graph_from_weighted_sparse_matrix, +) import os import math @@ -2195,14 +2195,20 @@ def WeightedAdjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=Tru if (sparse is not None) and isinstance(matrix, sparse.spmatrix): return _graph_from_weighted_sparse_matrix( - klass, matrix, mode=mode, attr=attr, + klass, + matrix, + mode=mode, + attr=attr, ) if (np is not None) and isinstance(matrix, np.ndarray): matrix = matrix.tolist() return super().WeightedAdjacency( - matrix, mode=mode, attr=attr, loops=loops, + matrix, + mode=mode, + attr=attr, + loops=loops, ) def write_dimacs(self, f, source=None, target=None, capacity="capacity"): diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index 101a0c10f..4d1d8e1d2 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -40,7 +40,7 @@ # Logic to get graph from scipy sparse matrix. This would be simple if there # weren't so many modes. def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): - '''Construct graph from sparse matrix, unweighted''' + """Construct graph from sparse matrix, unweighted""" # This function assumes there is scipy and the matrix is a scipy sparse # matrix. The caller should make sure those conditions are met. from scipy import sparse @@ -111,12 +111,12 @@ def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): ) -def _graph_from_weighted_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED, attr='weight'): - '''Construct graph from sparse matrix, weighted +def _graph_from_weighted_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED, attr="weight"): + """Construct graph from sparse matrix, weighted NOTE: Of course, you cannot emcompass a fully general weighted multigraph with a single adjacency matrix, so we don't try to do it here either. - ''' + """ # This function assumes there is scipy and the matrix is a scipy sparse # matrix. The caller should make sure those conditions are met. from scipy import sparse From d8b1e3f1fb26f2cb8a1ce86b9fe9d11277dfec8d Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 30 Jan 2021 10:46:09 +1100 Subject: [PATCH 0150/1681] Add tests --- tests/test_generators.py | 99 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index f2d26c75a..1e296c3ee 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,11 +1,20 @@ import unittest from igraph import * + try: import numpy as np - import pandas as pd except ImportError: np = None + +try: + import scipy.sparse as sparse +except ImportError: + sparse = None + +try: + import pandas as pd +except ImportError: pd = None @@ -169,6 +178,50 @@ def testSBM(self): pref_matrix[0][1] = 0.7 self.assertRaises(InternalError, Graph.SBM, 60, pref_matrix, types) + @unittest.skipIf(np is None, "test case depends on NumPy") + def testAdjacencyNumPy(self): + mat = np.array( + [[0, 1, 1, 0], [1, 0, 0, 0], [0, 0, 2, 0], [0, 1, 0, 0]], + ) + + # ADJ_DIRECTED (default) + g = Graph.Adjacency(mat) + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + + # ADJ MIN + g = Graph.Adjacency(mat, mode="min") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (2, 2), (2, 2)]) + + # ADJ LOWER + g = Graph.Adjacency(mat, mode="lower") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (2, 2), (2, 2), (1, 3)]) + + @unittest.skipIf( + (sparse is None) or (np is None), "test case depends on NumPy/SciPy" + ) + def testSparseAdjacency(self): + mat = sparse.coo_matrix( + [[0, 1, 1, 0], [1, 0, 0, 0], [0, 0, 2, 0], [0, 1, 0, 0]], + ) + + # ADJ_DIRECTED (default) + g = Graph.Adjacency(mat) + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) + + # ADJ MIN + g = Graph.Adjacency(mat, mode="min") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (2, 2), (2, 2)]) + + # ADJ LOWER + g = Graph.Adjacency(mat, mode="lower") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (2, 2), (2, 2), (1, 3)]) + def testWeightedAdjacency(self): mat = [[0, 1, 2, 0], [2, 0, 0, 0], [0, 0, 2.5, 0], [0, 1, 0, 0]] @@ -187,6 +240,50 @@ def testWeightedAdjacency(self): self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + @unittest.skipIf(np is None, "test case depends on NumPy") + def testWeightedAdjacencyNumPy(self): + mat = np.array( + [[0, 1, 2, 0], [2, 0, 0, 0], [0, 0, 2.5, 0], [0, 1, 0, 0]], + ) + + g = Graph.Weighted_Adjacency(mat, attr="w0") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + + g = Graph.Weighted_Adjacency(mat, mode="plus") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + + @unittest.skipIf( + (sparse is None) or (np is None), "test case depends on NumPy/SciPy" + ) + def testSparseWeighedAdjacency(self): + mat = sparse.coo_matrix( + [[0, 1, 2, 0], [2, 0, 0, 0], [0, 0, 2.5, 0], [0, 1, 0, 0]] + ) + + g = Graph.Weighted_Adjacency(mat, attr="w0") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) + self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) + + g = Graph.Weighted_Adjacency(mat, mode="plus") + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) + self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + + g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) + el = g.get_edgelist() + self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) + self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) + @unittest.skipIf((np is None) or (pd is None), "test case depends on NumPy/Pandas") def testDataFrame(self): edges = pd.DataFrame( From 085c8cc6fb92e3a84614f033da3cb761c3bba371 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 31 Jan 2021 13:33:30 +0100 Subject: [PATCH 0151/1681] fix: build igraph library with -fPIC on Linux --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index a522c89b8..25f7c70a2 100644 --- a/setup.py +++ b/setup.py @@ -480,6 +480,10 @@ def compile_in(self, source_folder, build_folder, install_folder): for deps in "ARPACK BLAS CXSPARSE GLPK LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") + # -fPIC is needed on Linux so we can link to a static igraph lib from a + # Python shared library + args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") + retcode = subprocess.call(args) if retcode: return False From f6281336a44d38faba65c2768e908141890a970f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 4 Feb 2021 19:40:43 +0100 Subject: [PATCH 0152/1681] fix: remove unneeded reference to igraph_i_fdiv() --- src/_igraph/igraphmodule.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 8b4da52df..392cdada5 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -42,8 +42,6 @@ #define IGRAPH_MODULE #include "igraphmodule_api.h" -extern double igraph_i_fdiv(double, double); - /** * \defgroup python_interface Python module implementation * \brief Functions implementing a Python interface to \a igraph From 4374f420f43c40e195bd335a526081164bd13513 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 6 Feb 2021 14:50:00 +0100 Subject: [PATCH 0153/1681] updated igraph C library, fixed deprecation warnings --- src/_igraph/attributes.c | 2 +- tests/test_basic.py | 6 +++--- vendor/source/igraph | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index f0a027fa1..4090bc04f 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -1884,7 +1884,7 @@ static igraph_attribute_table_t igraphmodule_attribute_table = { }; void igraphmodule_initialize_attribute_handler(void) { - igraph_i_set_attribute_table(&igraphmodule_attribute_table); + igraph_set_attribute_table(&igraphmodule_attribute_table); } /** diff --git a/tests/test_basic.py b/tests/test_basic.py index 67367d504..28bca91e9 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -338,14 +338,14 @@ def testAdjacency(self): self.assertTrue(g.get_adjlist(IN) == [[2], [0], [1], [2]]) self.assertTrue(g.get_adjlist(ALL) == [[1, 2], [0, 2], [0, 1, 3], [2]]) - def testEdgeIncidency(self): + def testEdgeIncidence(self): g = Graph(4, [(0, 1), (1, 2), (2, 0), (2, 3)], directed=True) self.assertTrue(g.incident(2) == [2, 3]) self.assertTrue(g.incident(2, IN) == [1]) - self.assertTrue(g.incident(2, ALL) == [2, 3, 1]) + self.assertTrue(g.incident(2, ALL) == [2, 1, 3]) self.assertTrue(g.get_inclist() == [[0], [1], [2, 3], []]) self.assertTrue(g.get_inclist(IN) == [[2], [0], [1], [3]]) - self.assertTrue(g.get_inclist(ALL) == [[0, 2], [1, 0], [2, 3, 1], [3]]) + self.assertTrue(g.get_inclist(ALL) == [[0, 2], [0, 1], [2, 1, 3], [3]]) def testMultiplesLoops(self): g = Graph.Tree(7, 2) diff --git a/vendor/source/igraph b/vendor/source/igraph index 869be278f..c4c45e7ca 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 869be278ffa5bc97e3cb7b08f58d2b111c9dd47b +Subproject commit c4c45e7ca681ab0be76bfc8badb2ab95c9610877 From 7d3a3d8d91f8cb69e485f206cf69a94a446d7114 Mon Sep 17 00:00:00 2001 From: Gabor Szarnyas Date: Sat, 6 Feb 2021 15:55:48 +0100 Subject: [PATCH 0154/1681] Fix link in docs --- doc/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index b5aeb1599..22b16492f 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -24,7 +24,7 @@ are often called *binary packages*, while the raw source code is usually referre In general, you should almost always opt for the binary package unless a binary package is not available for your platform or you have some local modifications that you want to incorporate into |igraph|'s source. `Installation from a binary package`_ tells you how to -install |igraph| from a precompiled binary package on various platforms. `Compiling igraph +install |igraph| from a precompiled binary package on various platforms. `Compiling python-igraph from source`_ tells you how to compile |igraph| from the source package. Installation from a binary package From 60b2b88e0fe3a1d0a9f6fc4e70cbe05f3c7a01fd Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 6 Feb 2021 07:52:02 +1100 Subject: [PATCH 0155/1681] Remove star imports --- src/igraph/__init__.py | 122 ++++++++++++++++++++++++++++---- src/igraph/drawing/__init__.py | 2 +- src/igraph/drawing/colors.py | 4 +- src/igraph/drawing/edge.py | 4 +- src/igraph/drawing/graph.py | 2 +- src/igraph/drawing/metamagic.py | 2 +- src/igraph/drawing/shapes.py | 2 +- src/igraph/drawing/text.py | 2 +- src/igraph/drawing/utils.py | 2 +- src/igraph/drawing/vertex.py | 2 +- src/igraph/formula.py | 2 +- src/igraph/remote/gephi.py | 2 +- src/igraph/statistics.py | 4 +- src/igraph/summary.py | 2 +- src/igraph/test/foreign.py | 3 +- src/igraph/utils.py | 5 +- 16 files changed, 126 insertions(+), 36 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 6f05e44ac..fcd7ae25e 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -28,22 +28,114 @@ 02110-1301 USA """ -# pylint: disable-msg=W0401 -# W0401: wildcard import -from igraph._igraph import * -from igraph.clustering import * -from igraph.cut import * +from igraph._igraph import ( + ADJ_DIRECTED, + ADJ_LOWER, + ADJ_MAX, + ADJ_MIN, + ADJ_PLUS, + ADJ_UNDIRECTED, + ADJ_UPPER, + ALL, + ARPACKOptions, + BFSIter, + BLISS_F, + BLISS_FL, + BLISS_FLM, + BLISS_FM, + BLISS_FS, + BLISS_FSM, + DFSIter, + Edge, + EdgeSeq, + GET_ADJACENCY_BOTH, + GET_ADJACENCY_LOWER, + GET_ADJACENCY_UPPER, + GraphBase, + IN, + InternalError, + OUT, + REWIRING_SIMPLE, + REWIRING_SIMPLE_LOOPS, + STAR_IN, + STAR_MUTUAL, + STAR_OUT, + STAR_UNDIRECTED, + STRONG, + TRANSITIVITY_NAN, + TRANSITIVITY_ZERO, + TREE_IN, + TREE_OUT, + TREE_UNDIRECTED, + Vertex, + VertexSeq, + WEAK, + arpack_options, + community_to_membership, + convex_hull, + is_degree_sequence, + is_graphical_degree_sequence, + set_progress_handler, + set_random_number_generator, + set_status_handler, +) +from igraph.clustering import ( + Clustering, + VertexClustering, + Dendrogram, + VertexDendrogram, + Cover, + VertexCover, + CohesiveBlocks, +) +from igraph.cut import Cut, Flow from igraph.configuration import Configuration -from igraph.drawing import * -from igraph.drawing.colors import * -from igraph.datatypes import * -from igraph.formula import * -from igraph.layout import * -from igraph.matching import * -from igraph.operators import * -from igraph.statistics import * -from igraph.summary import * -from igraph.utils import * +from igraph.drawing import BoundingBox, DefaultGraphDrawer, Plot, Point, Rectangle, plot +from igraph.drawing.colors import ( + Palette, + GradientPalette, + AdvancedGradientPalette, + RainbowPalette, + PrecalculatedPalette, + ClusterColoringPalette, + color_name_to_rgb, + color_name_to_rgba, + hsv_to_rgb, + hsva_to_rgba, + hsl_to_rgb, + hsla_to_rgba, + rgb_to_hsv, + rgba_to_hsva, + rgb_to_hsl, + rgba_to_hsla, + palettes, + known_colors, +) +from igraph.datatypes import Matrix, DyadCensus, TriadCensus, UniqueIdGenerator +from igraph.formula import construct_graph_from_formula +from igraph.layout import Layout +from igraph.matching import Matching +from igraph.operators import disjoint_union, union, intersection +from igraph.statistics import ( + FittedPowerLaw, + Histogram, + RunningMean, + mean, + median, + percentile, + quantile, + power_law_fit, +) +from igraph.summary import GraphSummary +from igraph.utils import ( + dbl_epsilon, + multidict, + named_temporary_file, + numpy_to_contiguous_memoryview, + rescale, + safemin, + safemax, +) from igraph.version import __version__, __version_info__ import os diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 9e352fa31..9b91a4f08 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -36,7 +36,7 @@ ) from igraph.utils import _is_running_in_ipython, named_temporary_file -__all__ = ["BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot"] +__all__ = ("BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot") __license__ = "GPL" diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index c77487667..eedaa1fdb 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -28,7 +28,7 @@ from igraph.utils import str_to_orientation from math import ceil -__all__ = [ +__all__ = ( "Palette", "GradientPalette", "AdvancedGradientPalette", @@ -47,7 +47,7 @@ "rgba_to_hsla", "palettes", "known_colors", -] + ) class Palette(object): diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 1afdfd0cc..100cee5c6 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -2,14 +2,14 @@ Drawers for various edge styles in graph plots. """ -__all__ = [ +__all__ = ( "AbstractEdgeDrawer", "AlphaVaryingEdgeDrawer", "ArrowEdgeDrawer", "DarkToLightEdgeDrawer", "LightToDarkEdgeDrawer", "TaperedEdgeDrawer", -] +) __license__ = "GPL" diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 6d59b5b66..416ff4dfc 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -34,7 +34,7 @@ from igraph.drawing.vertex import DefaultVertexDrawer from igraph.layout import Layout -__all__ = ["DefaultGraphDrawer", "UbiGraphDrawer", "CytoscapeGraphDrawer"] +__all__ = ("DefaultGraphDrawer", "UbiGraphDrawer", "CytoscapeGraphDrawer") __license__ = "GPL" cairo = find_cairo() diff --git a/src/igraph/drawing/metamagic.py b/src/igraph/drawing/metamagic.py index 3e78a39da..164aef3a9 100644 --- a/src/igraph/drawing/metamagic.py +++ b/src/igraph/drawing/metamagic.py @@ -65,7 +65,7 @@ class VisualEdgeBuilder(AttributeCollectorBase): from igraph.configuration import Configuration -__all__ = ["AttributeSpecification", "AttributeCollectorBase"] +__all__ = ("AttributeSpecification", "AttributeCollectorBase") # pylint: disable-msg=R0903 # R0903: too few public methods diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index 2a8882189..0a3cd81a9 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -17,7 +17,7 @@ """ -__all__ = ["ShapeDrawerDirectory"] +__all__ = ("ShapeDrawerDirectory",) __license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index 323e8ec20..c6bd385cf 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -9,7 +9,7 @@ from igraph.drawing.baseclasses import AbstractCairoDrawer from warnings import warn -__all__ = ["TextAlignment", "TextDrawer"] +__all__ = ("TextAlignment", "TextDrawer") __license__ = "GPL" __docformat__ = "restructuredtext en" diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 32ba062f8..cde3e5266 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -6,7 +6,7 @@ from math import atan2, cos, sin from operator import itemgetter -__all__ = ["BoundingBox", "FakeModule", "Point", "Rectangle"] +__all__ = ("BoundingBox", "FakeModule", "Point", "Rectangle") __license__ = "GPL" ##################################################################### diff --git a/src/igraph/drawing/vertex.py b/src/igraph/drawing/vertex.py index bc6d6b2a8..9f4d082a0 100644 --- a/src/igraph/drawing/vertex.py +++ b/src/igraph/drawing/vertex.py @@ -10,7 +10,7 @@ from igraph.drawing.shapes import ShapeDrawerDirectory from math import pi -__all__ = ["AbstractVertexDrawer", "AbstractCairoVertexDrawer", "DefaultVertexDrawer"] +__all__ = ("AbstractVertexDrawer", "AbstractCairoVertexDrawer", "DefaultVertexDrawer") __license__ = "GPL" diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 78d826f1c..8d2fce8c4 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -15,7 +15,7 @@ import tokenize import token -__all__ = ["construct_graph_from_formula"] +__all__ = ("construct_graph_from_formula",) __license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz diff --git a/src/igraph/remote/gephi.py b/src/igraph/remote/gephi.py index 67f48c90e..5cda23739 100644 --- a/src/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -18,7 +18,7 @@ json = FakeModule() -__all__ = ["GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat"] +__all__ = ("GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat") __docformat__ = "restructuredtext en" __license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index b62199244..79c820ce9 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -26,7 +26,7 @@ import math -__all__ = [ +__all__ = ( "FittedPowerLaw", "Histogram", "RunningMean", @@ -35,7 +35,7 @@ "percentile", "quantile", "power_law_fit", -] + ) class FittedPowerLaw(object): diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 2fda39ab0..6dbffe1c5 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -11,7 +11,7 @@ from texttable import Texttable from textwrap import TextWrapper -__all__ = ["GraphSummary"] +__all__ = ("GraphSummary",) __license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz diff --git a/src/igraph/test/foreign.py b/src/igraph/test/foreign.py index eabaea1fd..49a4fa5d2 100644 --- a/src/igraph/test/foreign.py +++ b/src/igraph/test/foreign.py @@ -1,8 +1,9 @@ +import sys import io import unittest import warnings -from igraph import * +from igraph import Graph, InternalError from igraph.test.utils import temporary_file diff --git a/src/igraph/utils.py b/src/igraph/utils.py index b60eb666e..707afb0da 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -7,10 +7,7 @@ from contextlib import contextmanager -try: - from collections.abc import MutableMapping -except ImportError: - from collections import MutableMapping +from collections.abc import MutableMapping from ctypes import c_double, sizeof from itertools import chain From 55992adcc1ed7b6880dcdeb87189845121cf5908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Mon, 8 Feb 2021 11:03:30 +0100 Subject: [PATCH 0156/1681] added some missing imports that the test cases are complaining about --- src/igraph/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index fcd7ae25e..23116f551 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -74,6 +74,7 @@ community_to_membership, convex_hull, is_degree_sequence, + is_graphical, is_graphical_degree_sequence, set_progress_handler, set_random_number_generator, @@ -87,6 +88,7 @@ Cover, VertexCover, CohesiveBlocks, + compare_communities, ) from igraph.cut import Cut, Flow from igraph.configuration import Configuration From 2332e35922763253b4e3a656045e230a13f3eb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Mon, 8 Feb 2021 11:10:09 +0100 Subject: [PATCH 0157/1681] we also need split_join_distance in the main igraph module --- src/igraph/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 23116f551..798c16d4d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -89,6 +89,7 @@ VertexCover, CohesiveBlocks, compare_communities, + split_join_distance, ) from igraph.cut import Cut, Flow from igraph.configuration import Configuration From 56a047a095c385de2c0f46ff14609c57770c6639 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Feb 2021 11:53:31 +0100 Subject: [PATCH 0158/1681] chore: blackened source [ci skip] --- src/igraph/app/__init__.py | 1 - src/igraph/clustering.py | 14 +++++++++----- src/igraph/drawing/colors.py | 2 +- src/igraph/remote/__init__.py | 1 - src/igraph/statistics.py | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/igraph/app/__init__.py b/src/igraph/app/__init__.py index 1dd82a624..049f302eb 100644 --- a/src/igraph/app/__init__.py +++ b/src/igraph/app/__init__.py @@ -1,2 +1 @@ """User interfaces of igraph""" - diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 43dfee021..a7e7d9238 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -684,15 +684,19 @@ def summary(self, verbosity=0, max_leaf_count=40): @return: the summary of the dendrogram as a string. """ out = StringIO() - print("Dendrogram, %d elements, %d merges" % ( - self._nitems, - self._nmerges, - ), file=out) + print( + "Dendrogram, %d elements, %d merges" + % ( + self._nitems, + self._nmerges, + ), + file=out, + ) if self._nitems == 0 or verbosity < 1 or self._nitems > max_leaf_count: return out.getvalue().strip() - print('', file=out) + print("", file=out) positions = [None] * self._nitems inorder = self._traverse_inorder() diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index eedaa1fdb..049cfeb99 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -47,7 +47,7 @@ "rgba_to_hsla", "palettes", "known_colors", - ) +) class Palette(object): diff --git a/src/igraph/remote/__init__.py b/src/igraph/remote/__init__.py index e9e07dbc1..ba7da4dc7 100644 --- a/src/igraph/remote/__init__.py +++ b/src/igraph/remote/__init__.py @@ -1,2 +1 @@ """Classes that help igraph communicate with remote applications.""" - diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 79c820ce9..dda450851 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -35,7 +35,7 @@ "percentile", "quantile", "power_law_fit", - ) +) class FittedPowerLaw(object): From 2893b07020b54345934d939f56fcf96d3b558f40 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 6 Feb 2021 08:06:48 +1100 Subject: [PATCH 0159/1681] Add harmonic centrality --- src/_igraph/graphobject.c | 114 +++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bab3391ac..3e6bb5ddf 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4118,6 +4118,85 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, return list; } +/** \ingroup python_interface_graph + * \brief Calculates the harmonic centrality of some vertices in a graph. + * \return the harmonic centralities as a list (or a single float) + * \sa igraph_closeness + */ +PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + static char *kwlist[] = { "vertices", "mode", "cutoff", "weights", + "normalized", NULL }; + PyObject *vobj = Py_None, *list = NULL, *cutoff = Py_None, + *mode_o = Py_None, *weights_o = Py_None, *normalized_o = Py_True; + igraph_vector_t res, *weights = 0; + igraph_neimode_t mode = IGRAPH_ALL; + int return_single = 0; + igraph_vs_t vs; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, &vobj, + &mode_o, &cutoff, &weights_o, &normalized_o)) + return NULL; + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; + if (igraphmodule_PyObject_to_vs_t(vobj, &vs, &self->g, &return_single, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_init(&res, 0)) { + igraph_vs_destroy(&vs); + return igraphmodule_handle_igraph_error(); + } + + if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, + ATTRIBUTE_TYPE_EDGE)) { + igraph_vs_destroy(&vs); + igraph_vector_destroy(&res); + return NULL; + } + + if (cutoff == Py_None) { + if (igraph_harmonic_centrality(&self->g, &res, 0, 0, vs, mode, weights, + PyObject_IsTrue(normalized_o))) { + igraph_vs_destroy(&vs); + igraph_vector_destroy(&res); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + return NULL; + } + } else if (PyNumber_Check(cutoff)) { + PyObject *cutoff_num = PyNumber_Float(cutoff); + if (cutoff_num == NULL) { + igraph_vs_destroy(&vs); igraph_vector_destroy(&res); + return NULL; + } + if (igraph_harmonic_centrality_cutoff(&self->g, &res, 0, 0, vs, mode, weights, + (igraph_real_t)PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { + igraph_vs_destroy(&vs); + igraph_vector_destroy(&res); + if (weights) { igraph_vector_destroy(weights); free(weights); } + igraphmodule_handle_igraph_error(); + Py_DECREF(cutoff_num); + return NULL; + } + Py_DECREF(cutoff_num); + } + + if (weights) { igraph_vector_destroy(weights); free(weights); } + + if (!return_single) + list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); + else + list = PyFloat_FromDouble(VECTOR(res)[0]); + + igraph_vector_destroy(&res); + igraph_vs_destroy(&vs); + + return list; +} + /** \ingroup python_interface_graph * \brief Calculates the (weakly or strongly) connected components in a graph. * \return a list containing the cluster ID for every vertex in the graph @@ -12878,7 +12957,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "The closeness centerality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" "can be reached from the other vertices). It is defined as the\n" - "number of the number of vertices minus one divided by the sum of\n" + "number of vertices minus one divided by the sum of\n" "the lengths of all geodesics from/to the given vertex.\n\n" "If the graph is not connected, and there is no path between two\n" "vertices, the number of vertices is used instead the length of\n" @@ -12902,6 +12981,39 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " multiplying by the number of vertices minus one.\n" "@return: the calculated closenesses in a list\n"}, + /* interface to igraph_harmonic_centrality */ + {"harmonic_centrality", (PyCFunction) igraphmodule_Graph_harmonic_centrality, + METH_VARARGS | METH_KEYWORDS, + "harmonic_centrality(vertices=None, mode=ALL, cutoff=None, weights=None,\n" + " normalized=True)\n\n" + "Calculates the harmonic centralities of given vertices in a graph.\n\n" + "The harmonic centerality of a vertex measures how easily other\n" + "vertices can be reached from it (or the other way: how easily it\n" + "can be reached from the other vertices). It is defined as the\n" + "number of vertices minus one times the sum of one divided by \n" + "the length of each geodesic from/to the given vertex.\n\n" + "If the graph is not connected, and there is no path between two\n" + "vertices, the number of vertices is used instead the length of\n" + "the geodesic. This is always longer than the longest possible\n" + "geodesic.\n\n" + "@param vertices: the vertices for which the harmonic centrality must\n" + " be returned. If C{None}, uses all of the vertices in the graph.\n" + "@param mode: must be one of L{IN}, L{OUT} and L{ALL}. L{IN} means\n" + " that the length of the incoming paths, L{OUT} means that the\n" + " length of the outgoing paths must be calculated. L{ALL} means\n" + " that both of them must be calculated.\n" + "@param cutoff: if it is an integer, only paths less than or equal to this\n" + " length are considered, effectively resulting in an estimation of the\n" + " harmonic centrality for the given vertices (which is always an underestimation of the\n" + " real centrality, since some vertex pairs will appear as disconnected even\n" + " though they are connected).. If C{None}, the exact centrality is\n" + " returned.\n" + "@param weights: edge weights to be used. Can be a sequence or iterable or\n" + " even an edge attribute name.\n" + "@param normalized: Whether to normalize the raw centrality scores by\n" + " multiplying by the number of vertices minus one.\n" + "@return: the calculated harmonic centrality in a list\n"}, + /* interface to igraph_clusters */ {"clusters", (PyCFunction) igraphmodule_Graph_clusters, METH_VARARGS | METH_KEYWORDS, From 7ddb0a2375647897fc6ba0d877758fcccc4a3658 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 6 Feb 2021 09:41:27 +1100 Subject: [PATCH 0160/1681] Doc improvs --- src/_igraph/graphobject.c | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 3e6bb5ddf..a641ae6d5 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -12990,12 +12990,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "The harmonic centerality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" "can be reached from the other vertices). It is defined as the\n" - "number of vertices minus one times the sum of one divided by \n" - "the length of each geodesic from/to the given vertex.\n\n" + "mean inverse distance to all other vertices.\n\n" "If the graph is not connected, and there is no path between two\n" - "vertices, the number of vertices is used instead the length of\n" - "the geodesic. This is always longer than the longest possible\n" - "geodesic.\n\n" + "vertices, the inverse distance is taken to be zero.\n\n" "@param vertices: the vertices for which the harmonic centrality must\n" " be returned. If C{None}, uses all of the vertices in the graph.\n" "@param mode: must be one of L{IN}, L{OUT} and L{ALL}. L{IN} means\n" @@ -13003,15 +13000,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " length of the outgoing paths must be calculated. L{ALL} means\n" " that both of them must be calculated.\n" "@param cutoff: if it is an integer, only paths less than or equal to this\n" - " length are considered, effectively resulting in an estimation of the\n" - " harmonic centrality for the given vertices (which is always an underestimation of the\n" - " real centrality, since some vertex pairs will appear as disconnected even\n" - " though they are connected).. If C{None}, the exact centrality is\n" - " returned.\n" + " length are considered.\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param normalized: Whether to normalize the raw centrality scores by\n" - " multiplying by the number of vertices minus one.\n" + "@param normalized: Whether to normalize the result. If True, the \n" + " result is the mean inverse path length to other vertices, i.e. it \n" + " is normalized by the number of vertices minus one. If False, the \n" + " result is the sum of inverse path lengths to other vertices.\n" "@return: the calculated harmonic centrality in a list\n"}, /* interface to igraph_clusters */ From 907dbf2782441ae7f9c74488579b296af90c4fbc Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 6 Feb 2021 10:33:57 +1100 Subject: [PATCH 0161/1681] Doc mention and test --- doc/source/analysis.rst | 1 + tests/test_structural.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 8c15b4b83..1d3790c99 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -289,6 +289,7 @@ Structural: - :meth:`Graph.betweenness` - :meth:`Graph.bibcoupling` - :meth:`Graph.closeness` +- :meth:`Graph.harmonic_centrality` - :meth:`Graph.constraint` - :meth:`Graph.cocitation` - :meth:`Graph.coreness` (aka :meth:`Graph.shell_index`) diff --git a/tests/test_structural.py b/tests/test_structural.py index 1b5e25618..d4acfb5c6 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -355,6 +355,37 @@ def testClosenessCentrality(self): for obs, exp in zip(cl, expected_cl): self.assertAlmostEqual(obs, exp, places=4) + def testHarmonicCentrality(self): + g = Graph.Star(5) + cl = g.harmonic_centrality() + cl2 = [1.0] + [(1.0 + 1 / 2 * 3) / 4] * 4 + for idx in range(g.vcount()): + self.assertAlmostEqual(cl[idx], cl2[idx], places=3) + + g = Graph.Star(5) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cl = g.harmonic_centrality(cutoff=1.0) + cl2 = [1.0, 1.0, 1.0, 1.0, 1.0] + for idx in range(g.vcount()): + self.assertAlmostEqual(cl[idx], cl2[idx], places=3) + + weights = [1] * 4 + + g = Graph.Star(5) + cl = g.harmonic_centrality(weights=weights) + cl2 = [1.0] + [0.625] * 4 + for idx in range(g.vcount()): + self.assertAlmostEqual(cl[idx], cl2[idx], places=3) + + g = Graph.Star(5) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + cl = g.harmonic_centrality(cutoff=1.0, weights=weights) + cl2 = [1.0, 1.0, 1.0, 1.0, 1.0] + for idx in range(g.vcount()): + self.assertAlmostEqual(cl[idx], cl2[idx], places=3) + def testPageRank(self): g = Graph.Star(11) cent = g.pagerank() From cca96545af93387c34be62405709426a2ef05993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 10 Feb 2021 13:16:20 +0100 Subject: [PATCH 0162/1681] doc improvements to Graph.harmonic_centrality() Based on the suggestions of @szhorvat in the PR review --- src/_igraph/graphobject.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index a641ae6d5..1ed053a4f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -12999,15 +12999,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " that the length of the incoming paths, L{OUT} means that the\n" " length of the outgoing paths must be calculated. L{ALL} means\n" " that both of them must be calculated.\n" - "@param cutoff: if it is an integer, only paths less than or equal to this\n" + "@param cutoff: if it is not C{None}, only paths less than or equal to this\n" " length are considered.\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" - "@param normalized: Whether to normalize the result. If True, the \n" - " result is the mean inverse path length to other vertices, i.e. it \n" - " is normalized by the number of vertices minus one. If False, the \n" + "@param normalized: Whether to normalize the result. If True, the\n" + " result is the mean inverse path length to other vertices, i.e. it\n" + " is normalized by the number of vertices minus one. If False, the\n" " result is the sum of inverse path lengths to other vertices.\n" - "@return: the calculated harmonic centrality in a list\n"}, + "@return: the calculated harmonic centralities in a list\n"}, /* interface to igraph_clusters */ {"clusters", (PyCFunction) igraphmodule_Graph_clusters, From 767469957080daba889db041663c84d24a1e50ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 10 Feb 2021 13:19:45 +0100 Subject: [PATCH 0163/1681] Keep alphabetic ordering of functions in the docs [ci skip] --- doc/source/analysis.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 1d3790c99..79ee0a437 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -289,12 +289,12 @@ Structural: - :meth:`Graph.betweenness` - :meth:`Graph.bibcoupling` - :meth:`Graph.closeness` -- :meth:`Graph.harmonic_centrality` - :meth:`Graph.constraint` - :meth:`Graph.cocitation` - :meth:`Graph.coreness` (aka :meth:`Graph.shell_index`) - :meth:`Graph.eccentricity` - :meth:`Graph.eigenvector_centrality` +- :meth:`Graph.harmonic_centrality` - :meth:`Graph.pagerank` - :meth:`Graph.personalized_pagerank` - :meth:`Graph.strength` From d8ae2365b0550cc4155113be7156c8a5640313bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 10 Feb 2021 13:33:22 +0100 Subject: [PATCH 0164/1681] fix argument list of igraph_harmonic_centrality() calls --- src/_igraph/graphobject.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 1ed053a4f..e0b3d8b31 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4158,7 +4158,7 @@ PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self } if (cutoff == Py_None) { - if (igraph_harmonic_centrality(&self->g, &res, 0, 0, vs, mode, weights, + if (igraph_harmonic_centrality(&self->g, &res, vs, mode, weights, PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); @@ -4172,7 +4172,7 @@ PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self igraph_vs_destroy(&vs); igraph_vector_destroy(&res); return NULL; } - if (igraph_harmonic_centrality_cutoff(&self->g, &res, 0, 0, vs, mode, weights, + if (igraph_harmonic_centrality_cutoff(&self->g, &res, vs, mode, weights, (igraph_real_t)PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); From bfdbd410beedbd3fca38f78538ae819b2a091885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 10 Feb 2021 13:47:45 +0100 Subject: [PATCH 0165/1681] fix test case of harmonic centrality --- tests/test_structural.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_structural.py b/tests/test_structural.py index d4acfb5c6..148cf8539 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -366,7 +366,7 @@ def testHarmonicCentrality(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.harmonic_centrality(cutoff=1.0) - cl2 = [1.0, 1.0, 1.0, 1.0, 1.0] + cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) @@ -382,7 +382,7 @@ def testHarmonicCentrality(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") cl = g.harmonic_centrality(cutoff=1.0, weights=weights) - cl2 = [1.0, 1.0, 1.0, 1.0, 1.0] + cl2 = [1.0, 0.25, 0.25, 0.25, 0.25] for idx in range(g.vcount()): self.assertAlmostEqual(cl[idx], cl2[idx], places=3) From 087b0c144ab1eb13a1e1e428e5ad90ff048040d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 10 Feb 2021 14:06:43 +0100 Subject: [PATCH 0166/1681] fix memory leak in Graph.closeness_centrality() and Graph.harmonic_centrality() --- src/_igraph/graphobject.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e0b3d8b31..816de5589 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4091,6 +4091,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, PyObject *cutoff_num = PyNumber_Float(cutoff); if (cutoff_num == NULL) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); + if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } if (igraph_closeness_cutoff(&self->g, &res, 0, 0, vs, mode, weights, @@ -4170,6 +4171,7 @@ PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self PyObject *cutoff_num = PyNumber_Float(cutoff); if (cutoff_num == NULL) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); + if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } if (igraph_harmonic_centrality_cutoff(&self->g, &res, vs, mode, weights, From 3d3b327e22ec9737f96eb37afa3dceee1383c67b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 15 Feb 2021 12:35:07 +0100 Subject: [PATCH 0167/1681] chore: updated igraph to development branch --- src/_igraph/convert.c | 60 ++++++++++++++++++++++++-------------- src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 2 +- src/_igraph/igraphmodule.c | 2 +- vendor/source/igraph | 2 +- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 4b050b550..4815315ca 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1266,7 +1266,7 @@ int igraphmodule_PyObject_to_vector_bool_t(PyObject *list, item=PySequence_GetItem(list, i); if (item) { VECTOR(*v)[i]=PyObject_IsTrue(item); - Py_DECREF(item); + Py_DECREF(item); } else { /* this should not happen, but we return anyway. * an IndexError exception was set by PySequence_GetItem @@ -1322,10 +1322,11 @@ PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, list=PyList_New(n); for (i=0; i 0 ? min_cols : 0; + for (i = 0; i < nr; i++) { + row = PySequence_GetItem(o, i); if (!PySequence_Check(row)) { Py_DECREF(row); PyErr_SetString(PyExc_TypeError, "matrix expected (list of sequences)"); return 1; } - n=PySequence_Size(row); + n = PySequence_Size(row); Py_DECREF(row); - if (n>nc) nc=n; + if (n > nc) { + nc = n; + } } igraph_matrix_init(m, nr, nc); - for (i=0; ig, (igraph_integer_t) vid, mode, &vids, &layers, &parents)) { igraphmodule_handle_igraph_error(); return NULL; diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 392cdada5..86cae3747 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -339,7 +339,7 @@ PyObject* igraphmodule_community_to_membership(PyObject *self, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!ll|O", kwlist, &PyList_Type, &merges_o, &nodes, &steps, &return_csize)) return NULL; - if (igraphmodule_PyList_to_matrix_t(merges_o, &merges)) return NULL; + if (igraphmodule_PyList_to_matrix_t_with_minimum_column_count(merges_o, &merges, 2)) return NULL; if (igraph_vector_init(&result, nodes)) { igraphmodule_handle_igraph_error(); diff --git a/vendor/source/igraph b/vendor/source/igraph index c4c45e7ca..7ac8a58e2 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit c4c45e7ca681ab0be76bfc8badb2ab95c9610877 +Subproject commit 7ac8a58e2925c2fd8d0afdcfd6b87e07569aa0d5 From 27407f5c98c8618ba4d2ca727736f3db7d37fdab Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 15 Feb 2021 12:35:52 +0100 Subject: [PATCH 0168/1681] fix: Graph.VertexSeq.find(name=x) now works correctly when x is an integer, fixes #367 --- src/igraph/__init__.py | 8 ++++++-- tests/test_vertexseq.py | 11 ++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 798c16d4d..4f22a459f 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -4400,8 +4400,12 @@ def find(self, *args, **kwds): else: name = None - if name is not None and isinstance(name, str): - args = [name] + if name is not None: + if isinstance(name, str): + args = [name] + else: + # put back what we popped + kwds["name"] = name if args: # Selecting first based on positional arguments, then checking diff --git a/tests/test_vertexseq.py b/tests/test_vertexseq.py index a359cc9aa..39037e4a1 100644 --- a/tests/test_vertexseq.py +++ b/tests/test_vertexseq.py @@ -341,13 +341,22 @@ def testGraphMethodProxying(self): def testBug73(self): # This is a regression test for igraph/python-igraph#73 g = Graph() - g.add_vertices(2) + g.add_vertices(3) g.vs[0]["name"] = 1 g.vs[1]["name"] = "h" + g.vs[2]["name"] = 17 self.assertEqual(1, g.vs.find("h").index) self.assertEqual(1, g.vs.find(1).index) self.assertEqual(0, g.vs.find(name=1).index) + self.assertEqual(2, g.vs.find(name=17).index) + + def testBug367(self): + # This is a regression test for igraph/python-igraph#367 + g = Graph() + g.add_vertices([1, 2, 5]) + self.assertEqual([1, 2, 5], g.vs["name"]) + self.assertEqual(2, g.vs.find(name=5).index) def suite(): From 6b8d652ad30577dbb6a95959027b35926cbff651 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Feb 2021 15:22:30 +0100 Subject: [PATCH 0169/1681] refactor: setup.py cleanup, getting rid of some old Python 2.x-based constructs --- setup.py | 305 +++++-------------------------------------------------- 1 file changed, 24 insertions(+), 281 deletions(-) diff --git a/setup.py b/setup.py index 25f7c70a2..73b432abe 100644 --- a/setup.py +++ b/setup.py @@ -21,14 +21,15 @@ from setuptools import setup, Command, Extension -import distutils.ccompiler import glob import shutil import subprocess import sys import sysconfig +from pathlib import Path from select import select +from shutil import which from time import sleep ########################################################################### @@ -48,20 +49,6 @@ def building_on_windows_msvc(): return platform.system() == "Windows" and sysconfig.get_platform() != "mingw" -def create_dir_unless_exists(*args): - """Creates a directory unless it exists already.""" - path = os.path.join(*args) - if not os.path.isdir(path): - os.makedirs(path) - - -def ensure_dir_does_not_exist(*args): - """Ensures that the given directory does not exist.""" - path = os.path.join(*args) - if os.path.isdir(path): - shutil.rmtree(path) - - def exclude_from_list(items, items_to_exclude): """Excludes certain items from a list, keeping the original order of the remaining items.""" @@ -69,20 +56,6 @@ def exclude_from_list(items, items_to_exclude): return [item for item in items if item not in itemset] -def find_executable(name): - """Finds an executable with the given name on the system PATH and returns - its full path. - """ - try: - from shutil import which - - return which(name) # Python 3.3 and above - except ImportError: - import distutils.spawn - - return distutils.spawn.find_executable(name) # Earlier than Python 3.3 - - def find_static_library(library_name, library_path): """Given the raw name of a library in `library_name`, tries to find a static library with this name in the given `library_path`. `library_path` @@ -161,76 +134,6 @@ def is_unix_like(platform=None): ) -def find_msvc_source_folder(folder=".", requires_built=False): - """Finds the folder that contains the MSVC-specific source of igraph if there - is any. Returns `None` if no such folder is found. Prints a warning if the - choice is ambiguous. - """ - all_msvc_dirs = glob.glob(os.path.join(folder, "igraph-*-msvc")) - if len(all_msvc_dirs) > 0: - if len(all_msvc_dirs) > 1: - print("More than one MSVC build directory (..\\..\\igraph-*-msvc) found!") - print( - "It could happen that setup.py uses the wrong one! Please remove all but the right one!\n\n" - ) - - msvc_builddir = all_msvc_dirs[-1] - if requires_built and not os.path.exists( - os.path.join(msvc_builddir, "Release") - ): - print( - "There is no 'Release' dir in the MSVC build directory\n(%s)" - % msvc_builddir - ) - print("Please build the MSVC build first!\n") - return None - - return msvc_builddir - else: - return None - - -def preprocess_fallback_config(): - """Preprocesses the fallback include and library paths depending on the - platform.""" - global LIBIGRAPH_FALLBACK_INCLUDE_DIRS - global LIBIGRAPH_FALLBACK_LIBRARY_DIRS - global LIBIGRAPH_FALLBACK_LIBRARIES - - if ( - platform.system() == "Windows" - and distutils.ccompiler.get_default_compiler() == "msvc" - ): - # if this setup is run in the source checkout *and* the igraph msvc was build, - # this code adds the right library and include dir - msvc_builddir = find_msvc_source_folder( - os.path.join("..", ".."), requires_built=True - ) - - if msvc_builddir is not None: - print("Using MSVC build dir: %s\n\n" % msvc_builddir) - LIBIGRAPH_FALLBACK_INCLUDE_DIRS = [os.path.join(msvc_builddir, "include")] - LIBIGRAPH_FALLBACK_LIBRARY_DIRS = [os.path.join(msvc_builddir, "Release")] - return True - else: - return False - - else: - return True - - -def quote_path_for_shell(s): - # On MinGW / MSYS, we need to use forward slash style and remove unsafe - # characters in order not to trip up the configure script - if "MSYSTEM" in os.environ: - s = s.replace("\\", "/") - if s[1:3] == ":/": - s = "/" + s[0] + s[2:] - - # Now the proper quoting - return "'" + s.replace("'", "'\\''") + "'" - - def wait_for_keypress(seconds): """Wait for a keypress or until the given number of seconds have passed, whichever happens first. @@ -278,8 +181,8 @@ class IgraphCCoreBuilder(object): """ def create_build_config_file(self, install_folder, libraries): - with open(os.path.join(install_folder, "build.cfg"), "w") as f: - f.write(repr(libraries)) + with (install_folder / "build.cfg").open("w") as fp: + fp.write(repr(libraries)) def parse_pkgconfig_file(self, filename): building_on_windows = building_on_windows_msvc() @@ -288,7 +191,7 @@ def parse_pkgconfig_file(self, filename): libraries = ["igraph"] else: libraries = [] - with open(filename, "r") as fp: + with filename.open("r") as fp: for line in fp: if line.startswith("Libs: ") or line.startswith("Libs.private: "): words = line.strip().split() @@ -303,145 +206,6 @@ def parse_pkgconfig_file(self, filename): return libraries -class IgraphCCoreAutotoolsBuilder(IgraphCCoreBuilder): - """Class responsible for downloading and building the C core of igraph - if it is not installed yet, assuming that the C core uses `configure.ac` - and its friends. This used to be the case before igraph 0.9. - """ - - def compile_in(self, source_folder, build_folder, install_folder): - """Compiles igraph from its source code in the given folder. - - source_folder is the name of the folder that contains igraph's source - files. build_folder is the name of the folder where the build should - be executed. Both must be absolute paths. - - Returns: - False if the build failed or the list of libraries to link to when - linking the Python interface to igraph - """ - build_to_source_folder = os.path.relpath(source_folder, build_folder) - - os.chdir(source_folder) - - # Run the bootstrap script if we have downloaded a tarball from - # Github - if os.path.isfile("bootstrap.sh") and not os.path.isfile("configure"): - print("Bootstrapping igraph...") - retcode = subprocess.call("sh bootstrap.sh", shell=True) - if retcode: - return False - - # Patch ltmain.sh so it does not freak out when the build directory - # contains spaces - with open("ltmain.sh") as infp: - with open("ltmain.sh.new", "w") as outfp: - for line in infp: - if line.endswith("cd $darwin_orig_dir\n"): - line = line.replace( - "cd $darwin_orig_dir\n", 'cd "$darwin_orig_dir"\n' - ) - outfp.write(line) - shutil.move("ltmain.sh.new", "ltmain.sh") - - os.chdir(build_folder) - - print("Configuring igraph...") - configure_args = ["--disable-tls", "--enable-silent-rules"] - if "IGRAPH_EXTRA_CONFIGURE_ARGS" in os.environ: - configure_args.extend(os.environ["IGRAPH_EXTRA_CONFIGURE_ARGS"].split(" ")) - retcode = subprocess.call( - "sh {0} {1}".format( - quote_path_for_shell(os.path.join(build_to_source_folder, "configure")), - " ".join(configure_args), - ), - env=self.enhanced_env(CFLAGS="-fPIC", CXXFLAGS="-fPIC"), - shell=True, - ) - if retcode: - return False - - building_on_windows = building_on_windows_msvc() - - if building_on_windows: - print("Creating Microsoft Visual Studio project...") - retcode = subprocess.call("make msvc", shell=True) - if retcode: - return False - - print("Building igraph...") - if building_on_windows: - msvc_source = find_msvc_source_folder() - if not msvc_source: - return False - - devenv = os.environ.get("DEVENV_EXECUTABLE") - os.chdir(msvc_source) - if devenv is None: - retcode = subprocess.call("devenv /upgrade igraph.vcproj", shell=True) - else: - retcode = subprocess.call([devenv, "/upgrade", "igraph.vcproj"]) - if retcode: - return False - - retcode = subprocess.call( - "msbuild.exe igraph.vcxproj /p:configuration=Release" - ) - else: - retcode = subprocess.call("make", shell=True) - - if retcode: - return False - - libraries = self.parse_pkgconfig_file("igraph.pc") - - return libraries - - def copy_build_artifacts( - self, source_folder, build_folder, install_folder, libraries - ): - building_on_windows = building_on_windows_msvc() - - create_dir_unless_exists(install_folder) - - ensure_dir_does_not_exist(install_folder, "include") - ensure_dir_does_not_exist(install_folder, "lib") - - shutil.copytree( - os.path.join(source_folder, "include"), - os.path.join(install_folder, "include"), - ) - create_dir_unless_exists(install_folder, "lib") - - for fname in glob.glob(os.path.join(build_folder, "include", "*.h")): - shutil.copy(fname, os.path.join(install_folder, "include")) - - if building_on_windows: - msvc_builddir = find_msvc_source_folder(build_folder, requires_built=True) - if msvc_builddir is not None: - print("Using MSVC build dir: %s\n\n" % msvc_builddir) - for fname in glob.glob(os.path.join(msvc_builddir, "Release", "*.lib")): - shutil.copy(fname, os.path.join(install_folder, "lib")) - else: - print("Cannot find MSVC build dir in %s\n\n" % build_folder) - return False - else: - for fname in glob.glob( - os.path.join(build_folder, "src", ".libs", "libigraph.*") - ): - shutil.copy(fname, os.path.join(install_folder, "lib")) - - return True - - @staticmethod - def enhanced_env(**kwargs): - env = os.environ.copy() - for k, v in kwargs.items(): - prev = os.environ.get(k) - env[k] = "{0} {1}".format(prev, v) if prev else v - return env - - ########################################################################### @@ -464,7 +228,7 @@ def compile_in(self, source_folder, build_folder, install_folder): """ global is_windows - cmake = find_executable("cmake") + cmake = which("cmake") if not cmake: print( "igraph uses CMake as the build system. You need to install CMake " @@ -476,7 +240,7 @@ def compile_in(self, source_folder, build_folder, install_folder): os.chdir(build_folder) print("Configuring build...") - args = [cmake, build_to_source_folder] + args = [cmake, str(build_to_source_folder)] for deps in "ARPACK BLAS CXSPARSE GLPK LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") @@ -496,17 +260,11 @@ def compile_in(self, source_folder, build_folder, install_folder): return False print("Installing build...") - retcode = subprocess.call([cmake, "--install", ".", "--prefix", install_folder]) + retcode = subprocess.call([cmake, "--install", ".", "--prefix", str(install_folder)]) if retcode: return False - return self.parse_pkgconfig_file(os.path.join(install_folder, "lib", "pkgconfig", "igraph.pc")) - - def copy_build_artifacts( - self, source_folder, build_folder, install_folder, libraries - ): - # Nothing to do; we already installed stuff in the compilation step - return True + return self.parse_pkgconfig_file(install_folder / "lib" / "pkgconfig" / "igraph.pc") ########################################################################### @@ -675,37 +433,34 @@ def compile_igraph_from_vendor_source(self): """Compiles igraph from the vendored source code inside `vendor/source/igraph`. This folder typically comes from a git submodule. """ - if os.path.exists(os.path.join("vendor", "install", "igraph")): + vendor_folder = Path("vendor") + source_folder = vendor_folder / "source" / "igraph" + build_folder = vendor_folder / "build" / "igraph" + install_folder = vendor_folder / "install" / "igraph" + + if install_folder.exists(): # Vendored igraph already compiled and installed, just use it self.use_vendored_igraph() return True - vendor_source_path = os.path.join("vendor", "source", "igraph") - if os.path.isfile(os.path.join(vendor_source_path, "CMakeLists.txt")): + if (source_folder / "CMakeLists.txt").exists(): igraph_builder = IgraphCCoreCMakeBuilder() - elif os.path.isfile(os.path.join(vendor_source_path, "configure.ac")): - igraph_builder = IgraphCCoreAutotoolsBuilder() else: - # No git submodule present with vendored source - print("Cannot find vendored igraph source in " + vendor_source_path) + print("Cannot find vendored igraph source in {0}".format(source_folder)) print("") return False - source_folder = os.path.join("vendor", "source", "igraph") - build_folder = os.path.join("vendor", "build", "igraph") - install_folder = os.path.join("vendor", "install", "igraph") - print("We are going to build the C core of igraph.") - print(" Source folder: %s" % source_folder) - print(" Build folder: %s" % build_folder) - print(" Install folder: %s" % install_folder) + print(" Source folder: {0}".format(source_folder)) + print(" Build folder: {0}".format(build_folder)) + print(" Install folder: {0}".format(install_folder)) print("") - source_folder = os.path.abspath(source_folder) - build_folder = os.path.abspath(build_folder) - install_folder = os.path.abspath(install_folder) + source_folder = source_folder.resolve() + build_folder = build_folder.resolve() + install_folder = install_folder.resolve() - create_dir_unless_exists(build_folder) + Path(build_folder).mkdir(parents=True, exist_ok=True) cwd = os.getcwd() try: @@ -717,16 +472,6 @@ def compile_igraph_from_vendor_source(self): finally: os.chdir(cwd) - if not igraph_builder.copy_build_artifacts( - source_folder=source_folder, - build_folder=build_folder, - install_folder=install_folder, - libraries=libraries, - ): - print("Could not compile the C core of igraph.") - print("") - sys.exit(1) - igraph_builder.create_build_config_file(install_folder, libraries) self.use_vendored_igraph() @@ -852,8 +597,6 @@ def use_educated_guess(self): """Tries to guess the proper library names, include and library paths if everything else failed.""" - preprocess_fallback_config() - global LIBIGRAPH_FALLBACK_LIBRARIES global LIBIGRAPH_FALLBACK_INCLUDE_DIRS global LIBIGRAPH_FALLBACK_LIBRARY_DIRS From b520fb8cd1764d5e86215153fc958ad78730faa1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Feb 2021 15:22:59 +0100 Subject: [PATCH 0170/1681] updating to igraph 0.9.0 --- src/_igraph/graphobject.c | 12 ++++++++---- vendor/source/igraph | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 126d3025d..6a5dcfd8a 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2273,7 +2273,7 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, if (igraph_establishment_game(&g, (igraph_integer_t) n, (igraph_integer_t) types, (igraph_integer_t) k, &td, &pm, - PyObject_IsTrue(directed))) { + PyObject_IsTrue(directed), 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); @@ -2894,7 +2894,7 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, types; + long n, in_types, out_types; PyObject *type_dist_matrix, *pref_matrix; PyObject *loops = Py_False; igraph_matrix_t pm; @@ -2913,13 +2913,15 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, &attribute_key, &loops)) return NULL; - types = PyList_Size(type_dist_matrix); if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm)) return NULL; if (igraphmodule_PyList_to_matrix_t(type_dist_matrix, &td)) { igraph_matrix_destroy(&pm); return NULL; } + in_types = igraph_matrix_nrow(&pm); + out_types = igraph_matrix_ncol(&pm); + store_attribs = (attribute_key && attribute_key != Py_None); if (store_attribs) { if (igraph_vector_init(&in_type_vec, (igraph_integer_t) n)) { @@ -2938,7 +2940,9 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, } if (igraph_asymmetric_preference_game(&g, (igraph_integer_t) n, - (igraph_integer_t) types, &td, &pm, + (igraph_integer_t) in_types, + (igraph_integer_t) out_types, + &td, &pm, store_attribs ? &in_type_vec : 0, store_attribs ? &out_type_vec : 0, PyObject_IsTrue(loops))) { diff --git a/vendor/source/igraph b/vendor/source/igraph index 7ac8a58e2..6bb4e76c1 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 7ac8a58e2925c2fd8d0afdcfd6b87e07569aa0d5 +Subproject commit 6bb4e76c1d0f971363034e23d06b238a423315cf From 9f8ba30eaec25863e1dbf7c0508e0ac2fcad6407 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 20 Feb 2021 21:22:06 +0100 Subject: [PATCH 0171/1681] doc: clarified that the 'fixed' argument is ineffective in the DrL layout --- src/_igraph/graphobject.c | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6a5dcfd8a..06dab05ac 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -6866,6 +6866,12 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, return NULL; if (fixed_o != 0 && fixed_o != Py_None) { + /* Apparently the "fixed" argument does not do anything in the DrL + * implementation so we throw a warning if the user tries to use it */ + PyErr_Warn(PyExc_DeprecationWarning, "The fixed=... argument of the DrL " + "layout is ignored; it is kept only for sake of backwards " + "compatibility. The DrL layout algorithm does not support " + "permanently fixed nodes."); fixed = (igraph_vector_bool_t*)malloc(sizeof(igraph_vector_bool_t)); if (!fixed) { PyErr_NoMemory(); @@ -14299,11 +14305,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param seed: if C{None}, uses a random starting layout for the\n" " algorithm. If a matrix (list of lists), uses the given matrix\n" " as the starting position.\n" - "@param fixed: if a seed is given, you can specify some vertices to be\n" - " kept fixed at their original position in the seed by passing an\n" - " appropriate list here. The list must have exactly as many items as\n" - " the number of vertices in the graph. Items of the list that evaluate\n" - " to C{True} denote vertices that will not be moved.\n" + "@param fixed: ignored. We used to assume that the DrL layout supports\n" + " fixed nodes, but later it turned out that the argument has no effect\n" + " in the original DrL code. We kept the argument for sake of backwards\n" + " compatibility, but it will have no effect on the final layout.\n" "@param options: if you give a string argument here, you can select from\n" " five default preset parameterisations: C{default}, C{coarsen} for a\n" " coarser layout, C{coarsest} for an even coarser layout, C{refine} for\n" From d2c30b226efa94a62629a7313a0960e8ae6bf138 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Feb 2021 21:18:01 +0100 Subject: [PATCH 0172/1681] remove Python 2.7 from badge; not supported any more [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b89b58c09..196de140f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Travis CI](https://img.shields.io/travis/igraph/python-igraph)](https://travis-ci.org/igraph/python-igraph) -[![PyPI pyversions](https://img.shields.io/badge/python-2.7%20%7C%203.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://pypi.python.org/pypi/python-igraph) +[![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://pypi.python.org/pypi/python-igraph) [![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) Python interface for the igraph library From 1b9d50957ac3bd313e7dbf65664629ea5d6c03ff Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Feb 2021 21:40:59 +0100 Subject: [PATCH 0173/1681] setup.py now accepts extra CMake arguments from environment --- setup.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 73b432abe..9a508814e 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ from setuptools import setup, Command, Extension import glob +import shlex import shutil import subprocess import sys @@ -240,7 +241,13 @@ def compile_in(self, source_folder, build_folder, install_folder): os.chdir(build_folder) print("Configuring build...") - args = [cmake, str(build_to_source_folder)] + args = [cmake] + + # Add any extra CMake args from environment variables + if "IGRAPH_CMAKE_EXTRA_ARGS" in os.environ: + args.extend(shlex.split(os.environ["IGRAPH_CMAKE_EXTRA_ARGS"])) + + # Build the Python interface with vendored libraries for deps in "ARPACK BLAS CXSPARSE GLPK LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") @@ -248,6 +255,9 @@ def compile_in(self, source_folder, build_folder, install_folder): # Python shared library args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") + # Finally, add the source folder path + args.append(str(build_to_source_folder)) + retcode = subprocess.call(args) if retcode: return False From 719c4d8e738c4e80bc1fb1813b354dd866d5ad6e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Feb 2021 21:44:12 +0100 Subject: [PATCH 0174/1681] ci: try to give a hint to CMake about the platform we are building for --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index dca9b2d48..98e059d0d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,18 +5,19 @@ environment: global: CIBW_BEFORE_BUILD: python setup.py build_c_core CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - IGRAPH_EXTRA_CONFIGURE_ARGS: "--disable-graphml" PYTHON: "C:\\Python37" matrix: - CIBW_BUILD: "*-win32" CIBW_SKIP: "pp27-* cp27-* cp35-*" + IGRAPH_EXTRA_CMAKE_ARGS: "-A Win32" MSYSTEM: MINGW32 PATH: C:\msys64\usr\bin;C:\msys64\mingw32\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x86" - CIBW_BUILD: "*-win_amd64" CIBW_SKIP: "pp27-* cp27-* cp35-*" + IGRAPH_EXTRA_CMAKE_ARGS: "-A x64" MSYSTEM: MINGW64 PATH: C:\msys64\usr\bin;C:\msys64\mingw64\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x64" From 99fc5ce097397d1516dd2e18cd300a4d10f1e5cd Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Feb 2021 22:04:29 +0100 Subject: [PATCH 0175/1681] ci: fix name of environment variable that passes extra args to cmake --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 98e059d0d..30e1daab8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,14 +10,14 @@ environment: matrix: - CIBW_BUILD: "*-win32" CIBW_SKIP: "pp27-* cp27-* cp35-*" - IGRAPH_EXTRA_CMAKE_ARGS: "-A Win32" + IGRAPH_CMAKE_EXTRA_ARGS: "-A Win32" MSYSTEM: MINGW32 PATH: C:\msys64\usr\bin;C:\msys64\mingw32\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x86" - CIBW_BUILD: "*-win_amd64" CIBW_SKIP: "pp27-* cp27-* cp35-*" - IGRAPH_EXTRA_CMAKE_ARGS: "-A x64" + IGRAPH_CMAKE_EXTRA_ARGS: "-A x64" MSYSTEM: MINGW64 PATH: C:\msys64\usr\bin;C:\msys64\mingw64\bin;C:\Windows\System32;C:\Windows;%PATH% TARGET_ARCH: "x64" From 15bb29a550db66501db171855ce569cb82db1fce Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 17:29:51 +0100 Subject: [PATCH 0176/1681] added changelog, documenting all major changes since python-igraph 0.8.3 --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..7c6acfca3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,64 @@ +# igraph Python interface changelog + +## [Unreleased] + +### Added + +* `Graph.DataFrame` now has a `use_vids=...` keyword argument that decides whether + the data frame contains vertex IDs (`True`) or vertex names (`False`). (PR #348) + +* Added `MatplotlibGraphDrawer` to draw a graph on an existing Matplotlib + figure. (PR #341) + +* Added a code path to choose between preferred image viewers on FreeBSD. (PR #354) + +* Added `Graph.harmonic_centrality()` that wraps `igraph_harmonic_centrality()` + from the underlying C library. + +### Changed + +* `python-igraph` is now compatible with `igraph` 0.9.0. + +* The setup script was adapted to the new CMake-based build system of `igraph`. + +* Dropped support for older Python versions; the oldest Python version that + `python-igraph` is tested on is now Python 3.6. + +* The default splitting heuristic of the BLISS isomorphism algorithm was changed + from `IGRAPH_BLISS_FM` (first maximally non-trivially connected non-singleton cell) + to `IGRAPH_BLISS_FL` (first largest non-singleton cell) as this seems to provide + better performance on a variety of graph classes. This change is a follow-up + of the change in the recommended heuristic in the core igraph C library. + +### Fixed + +* Fixed crashes in the Python-C glue code related to the handling of empty + vectors in certain attribute merging functions (see issue #358). + +* Fixed a memory leak in `Graph.closeness_centrality()` when an invalid `cutoff` + argument was provided to the function. + +* Clarified that the `fixed=...` argument is ineffective for the DrL layout + because the underlying C code does not handle it. The argument was _not_ + removed for sake of backwards compatibility. + +* `VertexSeq.find(name=x)` now works correctly when `x` is an integer; fixes + #367 + +### Miscellaneous + +* The Python codebase was piped through `black` for consistent formatting. + +* Wildcard imports were removed from the codebase. + +* CI tests were moved to Azure Pipelines from Travis. + +* The core C library is now built with `-fPIC` on Linux to allow linking to the + Python interface. + +## 0.8.3 + +This is the last released version of `python-igraph` without a changelog file. +Please refer to the commit logs at https://github.com/igraph/python-igraph for +a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 +are documented above. From e2b49bfaf617cce7bb122a9be429a38d7f860b61 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 18:13:03 +0100 Subject: [PATCH 0177/1681] chore: bumped version to 0.9.0 --- CHANGELOG.md | 2 +- doc/source/conf.py | 16 ++++++++-------- src/igraph/version.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6acfca3..f4a6fc05a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,7 @@ * Wildcard imports were removed from the codebase. -* CI tests were moved to Azure Pipelines from Travis. +* CI tests were moved to Github Actions from Travis. * The core C library is now built with `-fPIC` on Linux to allow linking to the Python interface. diff --git a/doc/source/conf.py b/doc/source/conf.py index 3a1483d4e..16be2bd7f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -44,16 +44,16 @@ # General information about the project. project = u'python-igraph' -copyright = u'2010-{0}, Tamás Nepusz, Gábor Csárdi'.format(datetime.now().year) +copyright = u'2010-{0}, The igraph development team'.format(datetime.now().year) # 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. # # The short X.Y version. -version = '0.8.3' +version = '0.9.0' # The full version, including alpha/beta/rc tags. -release = '0.8.3' +release = '0.9.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -185,7 +185,7 @@ # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'python-igraph.tex', u'python-igraph Documentation', - u'Tamas Nepusz, Gabor Csardi', 'manual'), + u'The igraph development team', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -218,7 +218,7 @@ # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'python-igraph', u'python-igraph Documentation', - [u'Tamas Nepusz, Gabor Csardi'], 1) + [u'The igraph development team'], 1) ] @@ -226,9 +226,9 @@ # Bibliographic Dublin Core info. epub_title = u'python-igraph' -epub_author = u'Tamas Nepusz, Gabor Csardi' -epub_publisher = u'Tamas Nepusz, Gabor Csardi' -epub_copyright = u'2010, Tamas Nepusz, Gabor Csardi' +epub_author = u'The igraph development team' +epub_publisher = u'The igraph development team' +epub_copyright = u'2010, The igraph development team' # The language of the text. It defaults to the language option # or en if the language is not set. diff --git a/src/igraph/version.py b/src/igraph/version.py index aea240828..1836d2e87 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 8, 3) +__version_info__ = (0, 9, 0) __version__ = ".".join("{0}".format(x) for x in __version_info__) From e6ca6ee969c6f2fdfa1b56128c10d74a065acc70 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 18:32:58 +0100 Subject: [PATCH 0178/1681] ci: it looks like we need a newer CMake in the Linux cibuildwheel env --- .github/workflows/deploy.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 4ee06b956..7e3a6781f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,15 +23,15 @@ jobs: - name: Install OS dependencies run: sudo apt-get install gfortran flex bison - - name: Install Python dependencies + - name: Install CMake and cibuildwheel run: | # Pypi has no pip by default, and ubuntu blocks python -m ensurepip # However, Github runners are supposed to have pip installed by default # https://docs.github.com/en/actions/guides/building-and-testing-python #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install cibuildwheel - - name: Wheels (linux) + pip install cmake cibuildwheel + - name: Wheels (Linux) env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" @@ -44,7 +44,7 @@ jobs: with: name: source-dist path: dist/python-igraph-*.tar.gz - - name: Upload linux wheels + - name: Upload Linux wheels uses: actions/upload-artifact@v2 with: name: linux-wheels @@ -72,7 +72,7 @@ jobs: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" CIBW_SKIP: "cp27-* pp27-* cp35-*" - - name: Upload linux wheels + - name: Upload Linux wheels uses: actions/upload-artifact@v2 with: name: OSX-wheels @@ -90,7 +90,7 @@ jobs: name: source-dist path: dist - - name: "Download artifacts: linux wheels" + - name: "Download artifacts: Linux wheels" uses: actions/download-artifact@v2 with: name: linux-wheels From f1196ae80233aca059d72d4d88d7b12ad6d02486 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 18:43:45 +0100 Subject: [PATCH 0179/1681] ci: cmake is needed _inside_ cibuildwheel, not in the host environment --- .github/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7e3a6781f..65ef5466e 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -23,17 +23,17 @@ jobs: - name: Install OS dependencies run: sudo apt-get install gfortran flex bison - - name: Install CMake and cibuildwheel + - name: Install cibuildwheel run: | # Pypi has no pip by default, and ubuntu blocks python -m ensurepip # However, Github runners are supposed to have pip installed by default # https://docs.github.com/en/actions/guides/building-and-testing-python #wget -qO- https://bootstrap.pypa.io/get-pip.py | python python -m pip install --upgrade pip - pip install cmake cibuildwheel + pip install cibuildwheel - name: Wheels (Linux) env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && python setup.py build_c_core" + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" CIBW_SKIP: "cp27-* pp27-* cp35-*" run: | @@ -69,7 +69,7 @@ jobs: run: | python -m cibuildwheel --output-dir wheelhouse env: - CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_BEFORE_BUILD: "pip install cmake && python setup.py build_c_core" CIBW_TEST_COMMAND: "cd {project} && python -m unittest" CIBW_SKIP: "cp27-* pp27-* cp35-*" - name: Upload Linux wheels From b88389553e1f5780a4b12c2c129da673ab053d2b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 19:10:08 +0100 Subject: [PATCH 0180/1681] ci: setup.py now also looks for igraph.pc in lib64 --- setup.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a508814e..b95fc67c0 100644 --- a/setup.py +++ b/setup.py @@ -274,7 +274,15 @@ def compile_in(self, source_folder, build_folder, install_folder): if retcode: return False - return self.parse_pkgconfig_file(install_folder / "lib" / "pkgconfig" / "igraph.pc") + pkgconfig_candidates = [ + install_folder / "lib" / "pkgconfig" / "igraph.pc", + install_folder / "lib64" / "pkgconfig" / "igraph.pc" + ] + for candidate in pkgconfig_candidates: + if candidate.exists(): + return self.parse_pkgconfig_file(candidate) + + raise RuntimeError("no igraph.pc was found in the installation folder of igraph") ########################################################################### From 83f8b7675d025633073520698a280646a749e4f7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 19:50:57 +0100 Subject: [PATCH 0181/1681] fix: use a slightly newer revision from upstream that fixes a problem w.r.t lib/ vs lib64/ in igraph.pc --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 6bb4e76c1..8bf4adbe3 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 6bb4e76c1d0f971363034e23d06b238a423315cf +Subproject commit 8bf4adbe32de8e0a746fbf8b31268ff040cbe175 From b45a51d963223d3d287fd3af101d05a716c8d08d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 20:19:28 +0100 Subject: [PATCH 0182/1681] fix: handle lib/ vs lib64/ properly in other places of setup.py --- setup.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index b95fc67c0..da407363b 100644 --- a/setup.py +++ b/setup.py @@ -600,16 +600,27 @@ def use_vendored_igraph(self): the include and library paths and the library names accordingly.""" building_on_windows = building_on_windows_msvc() - buildcfg.include_dirs = [os.path.join("vendor", "install", "igraph", "include", "igraph")] - buildcfg.library_dirs = [os.path.join("vendor", "install", "igraph", "lib")] + vendor_dir = Path("vendor") / "install" / "igraph" + + buildcfg.include_dirs = [str(vendor_dir / "include" / "igraph")] + buildcfg.library_dirs = [] + + for candidate in ("lib", "lib64"): + candidate = vendor_dir / candidate + if candidate.exists(): + buildcfg.library_dirs.append(str(candidate)) + break + else: + raise RuntimeError("cannot detect igraph library dir within " + str(vendor_dir)) + if not buildcfg.static_extension: buildcfg.static_extension = "only_igraph" if building_on_windows: buildcfg.define_macros.append(("IGRAPH_STATIC", "1")) - buildcfg_file = os.path.join("vendor", "install", "igraph", "build.cfg") - if os.path.exists(buildcfg_file): - buildcfg.libraries = eval(open(buildcfg_file).read()) + buildcfg_file = vendor_dir / "build.cfg" + if buildcfg_file.exists(): + buildcfg.libraries = eval(buildcfg_file.open("r").read()) def use_educated_guess(self): """Tries to guess the proper library names, include and library paths From abbef9bfddc964f15fd61cc0c5869ed1e0009a56 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 20:38:02 +0100 Subject: [PATCH 0183/1681] chore: finalize 0.9.0 release From 11baa7847ffb94c27d028313d1b0e45a96fccb6e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 20:52:16 +0100 Subject: [PATCH 0184/1681] fall back to official igraph 0.9.0 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 8bf4adbe3..6bb4e76c1 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 8bf4adbe32de8e0a746fbf8b31268ff040cbe175 +Subproject commit 6bb4e76c1d0f971363034e23d06b238a423315cf From 904d5d9f6fc6cb3867bde65df9186166df5f9781 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 20:55:03 +0100 Subject: [PATCH 0185/1681] chore: finalize changelog for 0.9.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a6fc05a..55496ca79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [Unreleased] +## [0.9.0] ### Added From e6653b76646abad646f2c2493e625302461d2f0a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Feb 2021 22:20:08 +0100 Subject: [PATCH 0186/1681] fix: update MANIFEST.in for new directory structure of the igraph repo --- MANIFEST.in | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 2278efcb5..9bfa4b090 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,12 +8,9 @@ include scripts/epydoc.cfg include tests/*.py graft vendor/source/igraph -prune vendor/source/igraph/doc/abstracts -prune vendor/source/igraph/doc/papers -prune vendor/source/igraph/doc/presentations +prune vendor/source/igraph/etc/abstracts +prune vendor/source/igraph/etc/papers +prune vendor/source/igraph/etc/presentations prune vendor/source/igraph/interfaces -prune vendor/source/igraph/nexus -prune vendor/source/igraph/tools/virtual -prune vendor/source/igraph/optional/simpleraytracer -prune vendor/source/igraph/tools/virtual +prune vendor/source/igraph/vendor/simpleraytracer From 9cd20858871fe7fac3d173553ea1013fde56f7c7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Feb 2021 13:20:45 +0100 Subject: [PATCH 0187/1681] doc: getting rid of Python 2.x constructs and fixing markup errors, preparing for transition from Epydoc --- scripts/epydoc-patched | 20 ----------- src/_igraph/edgeobject.c | 2 +- src/_igraph/vertexobject.c | 2 +- src/igraph/__init__.py | 8 ++++- src/igraph/app/shell.py | 4 +-- src/igraph/clustering.py | 41 ++++++++++++---------- src/igraph/configuration.py | 10 +++--- src/igraph/cut.py | 6 ++-- src/igraph/datatypes.py | 14 ++++---- src/igraph/drawing/__init__.py | 2 +- src/igraph/drawing/coord.py | 1 - src/igraph/drawing/text.py | 64 +++++++++++++--------------------- src/igraph/formula.py | 8 ++--- src/igraph/layout.py | 6 ++-- src/igraph/operators.py | 6 ++-- src/igraph/remote/gephi.py | 19 ++-------- src/igraph/statistics.py | 12 +++---- src/igraph/summary.py | 10 +----- src/igraph/utils.py | 5 +-- 19 files changed, 92 insertions(+), 148 deletions(-) delete mode 100755 scripts/epydoc-patched diff --git a/scripts/epydoc-patched b/scripts/epydoc-patched deleted file mode 100755 index e71bb35ce..000000000 --- a/scripts/epydoc-patched +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python -"""Patched version of Epydoc that does not blow up with `docutils` -newer than 0.6 when reST is used as a markup language""" - -from epydoc.cli import cli -from epydoc.markup.restructuredtext import parse_docstring -from epydoc.docwriter.latex import LatexWriter - -# Check whether Epydoc needs patching -doc = parse_docstring("aaa", []) -try: - doc.summary() -except AttributeError: - # Monkey-patching docutils so that Text nodes have a "data" property, - # which is always empty - from docutils.nodes import Text - Text.data="" - -LatexWriter.PREAMBLE += [r'\usepackage[T1]{fontenc}'] -cli() \ No newline at end of file diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index ab0d71b77..d438e9aa7 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -739,7 +739,7 @@ PyTypeObject igraphmodule_EdgeType = "The attributes of the edge can be accessed by using the edge\n" "as a hash:\n\n" " >>> e[\"weight\"] = 2 #doctest: +SKIP\n" - " >>> print e[\"weight\"] #doctest: +SKIP\n" + " >>> print(e[\"weight\"]) #doctest: +SKIP\n" " 2\n", // tp_doc 0, // tp_traverse 0, // tp_clear diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 9fe1b7495..834541834 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -873,7 +873,7 @@ PyTypeObject igraphmodule_VertexType = "The attributes of the vertex can be accessed by using the vertex\n" "as a hash:\n\n" " >>> v[\"color\"] = \"red\" #doctest: +SKIP\n" - " >>> print v[\"color\"] #doctest: +SKIP\n" + " >>> print(v[\"color\"]) #doctest: +SKIP\n" " red\n", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 4f22a459f..9d10f92c0 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3513,6 +3513,9 @@ def get_vertex_dataframe(self): If you want to use vertex names as index, you can do: + >>> from string import ascii_letters + >>> graph = Graph.GRG(25, 0.4) + >>> graph.vs["name"] = ascii_letters[:graph.vcount()] >>> df = graph.get_vertex_dataframe() >>> df.set_index('name', inplace=True) @@ -3543,6 +3546,9 @@ def get_edge_dataframe(self): If you want to use source and target vertex IDs as index, you can do: + >>> from string import ascii_letters + >>> graph = Graph.GRG(25, 0.4) + >>> graph.vs["name"] = ascii_letters[:graph.vcount()] >>> df = graph.get_edge_dataframe() >>> df.set_index(['source', 'target'], inplace=True) @@ -4591,7 +4597,7 @@ class EdgeSeq(_igraph.EdgeSeq): >>> g=Graph.Full(3) >>> for e in g.es: - ... print e.tuple + ... print(e.tuple) ... (0, 1) (0, 2) diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index 9b9293055..fcdf10eb8 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -43,14 +43,14 @@ class TerminalController: output to the terminal: >>> term = TerminalController() - >>> print 'This is '+term.GREEN+'green'+term.NORMAL + >>> print('This is '+term.GREEN+'green'+term.NORMAL) This is green Alternatively, the `render()` method can used, which replaces '${action}' with the string required to perform 'action': >>> term = TerminalController() - >>> print term.render('This is ${GREEN}green${NORMAL}') + >>> print(term.render('This is ${GREEN}green${NORMAL}')) This is green If the terminal doesn't support a given action, then the value of diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index a7e7d9238..7380e7e03 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -1,8 +1,6 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- -"""Classes related to graph clustering. - -@undocumented: _handle_mark_groups_arg_for_clustering, _prepare_community_comparison""" +"""Classes related to graph clustering.""" __license__ = u""" Copyright (C) 2006-2012 Tamás Nepusz @@ -63,7 +61,7 @@ class Clustering(object): of clusters: >>> for cluster in cl: - ... print " ".join(str(idx) for idx in cluster) + ... print(" ".join(str(idx) for idx in cluster)) ... 0 1 2 3 4 5 6 @@ -73,7 +71,7 @@ class Clustering(object): the clustering object to a list: >>> cluster_list = list(cl) - >>> print cluster_list + >>> print(cluster_list) [[0, 1, 2, 3], [4, 5, 6], [7, 8, 9, 10]] @undocumented: _formatted_cluster_iterator @@ -347,7 +345,7 @@ def cluster_graph(self, combine_vertices=None, combine_edges=None): See L{Graph.contract_vertices()} for more details. @param combine_edges: specifies how to derive the attributes of the edges in the new graph from the attributes of the old ones. See - L{Graph.simplify()} for more details. If you specify C{False} + L{igraph.Graph.simplify()} for more details. If you specify C{False} here, edges will not be combined, and the number of edges between the vertices representing the original clusters will be equal to the number of edges between the members of those clusters in the @@ -416,19 +414,21 @@ def _recalculate_modularity_safe(self): def subgraph(self, idx): """Get the subgraph belonging to a given cluster. + Precondition: the vertex set of the graph hasn't been modified since the + moment the cover was constructed. + @param idx: the cluster index @return: a copy of the subgraph - @precondition: the vertex set of the graph hasn't been modified since - the moment the clustering was constructed. """ return self._graph.subgraph(self[idx]) def subgraphs(self): """Gets all the subgraphs belonging to each of the clusters. + Precondition: the vertex set of the graph hasn't been modified since the + moment the cover was constructed. + @return: a list containing copies of the subgraphs - @precondition: the vertex set of the graph hasn't been modified since - the moment the clustering was constructed. """ return [self._graph.subgraph(cl) for cl in self] @@ -439,12 +439,13 @@ def giant(self): the clustering. It may also be known as the I{giant community} if the clustering represents the result of a community detection function. + Precondition: the vertex set of the graph hasn't been modified since the + moment the cover was constructed. + @note: there can be multiple largest clusters, this method will return the copy of an arbitrary one if there are multiple largest clusters. @return: a copy of the largest cluster. - @precondition: the vertex set of the graph hasn't been modified since - the moment the clustering was constructed. """ ss = self.sizes() max_size = max(ss) @@ -657,7 +658,7 @@ def format(self, format="newick"): if format == "newick": n = self._nitems + self._nmerges if self._names is None: - nodes = range(n) + nodes = list(range(n)) else: nodes = list(self._names) if len(nodes) < n: @@ -1098,7 +1099,7 @@ class Cover(object): clusters: >>> for cluster in cl: - ... print " ".join(str(idx) for idx in cluster) + ... print(" ".join(str(idx) for idx in cluster)) ... 0 1 2 3 2 3 4 @@ -1108,7 +1109,7 @@ class Cover(object): the cover to a list: >>> cluster_list = list(cl) - >>> print cluster_list + >>> print(cluster_list) [[0, 1, 2, 3], [2, 3, 4], [0, 1, 6]] L{Clustering} objects can readily be converted to L{Cover} objects @@ -1286,19 +1287,21 @@ def graph(self): def subgraph(self, idx): """Get the subgraph belonging to a given cluster. + Precondition: the vertex set of the graph hasn't been modified since the + moment the cover was constructed. + @param idx: the cluster index @return: a copy of the subgraph - @precondition: the vertex set of the graph hasn't been modified since - the moment the cover was constructed. """ return self._graph.subgraph(self[idx]) def subgraphs(self): """Gets all the subgraphs belonging to each of the clusters. + Precondition: the vertex set of the graph hasn't been modified since the + moment the cover was constructed. + @return: a list containing copies of the subgraphs - @precondition: the vertex set of the graph hasn't been modified since - the moment the cover was constructed. """ return [self._graph.subgraph(cl) for cl in self] diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 2a6921f21..1c507c4f1 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -109,9 +109,9 @@ class Configuration(object): This object provides an interface to the configuration data using the syntax known from dict: - >>> c=Configuration() + >>> c = Configuration() >>> c["general.verbose"] = True - >>> print c["general.verbose"] + >>> print(c["general.verbose"]) True Configuration keys are organized into sections, and the name to be used @@ -121,9 +121,9 @@ class Configuration(object): If the name of the section is omitted, it defaults to C{general}, so C{general.verbose} can be referred to as C{verbose}: - >>> c=Configuration() + >>> c = Configuration() >>> c["verbose"] = True - >>> print c["general.verbose"] + >>> print(c["general.verbose"]) True User-level configuration is stored in C{~/.igraphrc} per default on Linux @@ -195,8 +195,6 @@ class Configuration(object): - B{ipython.inlining.Plot}: whether to show instances of the L{Plot} class inline in IPython's console if the console supports it. Default: C{True} - - @undocumented: _item_to_section_key, _types, _sections, _definitions, _instance """ # pylint: disable-msg=R0903 diff --git a/src/igraph/cut.py b/src/igraph/cut.py index 0acbaf500..2d7a5d590 100644 --- a/src/igraph/cut.py +++ b/src/igraph/cut.py @@ -59,9 +59,9 @@ class Cut(VertexClustering): >>> from igraph import Graph >>> g = Graph.Ring(20) >>> mc = g.mincut() - >>> print mc.value + >>> print(mc.value) 2.0 - >>> print min(map(len, mc)) + >>> print(min(len(x) for x in mc)) 1 >>> mc.es["color"] = "red" """ @@ -165,7 +165,7 @@ class Flow(Cut): >>> from igraph import Graph >>> g = Graph.Ring(20) >>> mf = g.maxflow(0, 10) - >>> print mf.value + >>> print(mf.value) 2.0 >>> mf.es["color"] = "red" """ diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index b2bf6a17d..75a7bae5f 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -539,16 +539,14 @@ class DyadCensus(tuple): >>> from igraph import Graph >>> g=Graph.Erdos_Renyi(100, 0.2, directed=True) >>> dc=g.dyad_census() - >>> print dc.mutual #doctest:+SKIP + >>> print(dc.mutual) #doctest:+SKIP 179 - >>> print dc["asym"] #doctest:+SKIP + >>> print(dc["asym"]) #doctest:+SKIP 1609 - >>> print tuple(dc), list(dc) #doctest:+SKIP + >>> print(tuple(dc), list(dc)) #doctest:+SKIP (179, 1609, 3162) [179, 1609, 3162] - >>> print sorted(dc.as_dict().items()) #doctest:+ELLIPSIS + >>> print(sorted(dc.as_dict().items())) #doctest:+ELLIPSIS [('asymmetric', ...), ('mutual', ...), ('null', ...)] - - @undocumented: _remap """ _remap = { @@ -615,9 +613,9 @@ class TriadCensus(tuple): >>> from igraph import Graph >>> g=Graph.Erdos_Renyi(100, 0.2, directed=True) >>> tc=g.triad_census() - >>> print tc.t003 #doctest:+SKIP + >>> print(tc.t003) #doctest:+SKIP 39864 - >>> print tc["030C"] #doctest:+SKIP + >>> print(tc["030C"]) #doctest:+SKIP 1206 """ diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 9b91a4f08..364eef307 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -222,7 +222,7 @@ def background(self, color): """Sets the background color of the plot. C{None} means a transparent background. You can use any color specification here that is understood by the C{get} method of the current palette - or by L{igraph.colors.color_name_to_rgb}. + or by L{igraph.drawing.colors.color_name_to_rgb}. """ if color is None: self._background = None diff --git a/src/igraph/drawing/coord.py b/src/igraph/drawing/coord.py index 91db1a05a..a5e51e622 100644 --- a/src/igraph/drawing/coord.py +++ b/src/igraph/drawing/coord.py @@ -2,7 +2,6 @@ Coordinate systems and related plotting routines """ -from igraph.compat import property from igraph.drawing.baseclasses import AbstractCairoDrawer from igraph.drawing.utils import BoundingBox diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index c6bd385cf..6de3b3b9f 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -1,7 +1,5 @@ """ Drawers for labels on plots. - -@undocumented: test """ import re @@ -51,10 +49,8 @@ def draw(self, wrap=False): has an attribute named ``bbox`` which will be used as a bounding box. - :Parameters: - wrap : boolean - whether to allow re-wrapping of the text if it does not fit - within the bounding box horizontally. + @param wrap: whether to allow re-wrapping of the text if it does not + fit within the bounding box horizontally. """ ctx = self.context bbox = self.bbox @@ -90,26 +86,20 @@ def get_text_layout(self, x=None, y=None, width=None, wrap=False): Vertical alignment settings are not taken into account in this method as the text is not drawn within a box. - :Parameters: - x : float or ``None`` - The X coordinate of the reference point where the layout should + @param x: The X coordinate of the reference point where the layout should start. - y : float or ``None`` - The Y coordinate of the reference point where the layout should + @param y: The Y coordinate of the reference point where the layout should start. - width : float or ``None`` - The width of the box in which the text will be fitted. It matters - only when the text is right-aligned or centered. The text will - overflow the box if any of the lines is longer than the box width - and `wrap` is ``False``. - wrap : boolean - whether to allow re-wrapping of the text if it does not fit - within the given width. - - :Returns: - a list consisting of ``(x, y, line)`` tuples where ``x`` and ``y`` - refer to reference points on the Cairo canvas and ``line`` refers - to the corresponding text that should be plotted there. + @param width: The width of the box in which the text will be fitted. It + matters only when the text is right-aligned or centered. The text + will overflow the box if any of the lines is longer than the box + width and `wrap` is ``False``. + @param wrap: whether to allow re-wrapping of the text if it does not + fit within the given width. + + @return: a list consisting of ``(x, y, line)`` tuples where ``x`` and + ``y`` refer to reference points on the Cairo canvas and ``line`` + refers to the corresponding text that should be plotted there. """ ctx = self.context @@ -162,20 +152,16 @@ def draw_at(self, x=None, y=None, width=None, wrap=False): Vertical alignment settings are not taken into account in this method as the text is not drawn within a box. - :Parameters: - x : float or ``None`` - The X coordinate of the reference point where the drawing should + @param x: The X coordinate of the reference point where the layout should start. - y : float or ``None`` - The Y coordinate of the reference point where the drawing should + @param y: The Y coordinate of the reference point where the layout should start. - width : float or ``None`` - The width of the box in which the text will be fitted. It matters - only when the text is right-aligned or centered. The text will - overflow the box if any of the lines is longer than the box width. - wrap : boolean - whether to allow re-wrapping of the text if it does not fit - within the given width. + @param width: The width of the box in which the text will be fitted. It + matters only when the text is right-aligned or centered. The text + will overflow the box if any of the lines is longer than the box + width and `wrap` is ``False``. + @param wrap: whether to allow re-wrapping of the text if it does not + fit within the given width. """ ctx = self.context for ref_x, ref_y, line in self.get_text_layout(x, y, width, wrap): @@ -200,10 +186,8 @@ def _iterlines_wrapped(self, width): The difference between this method and `_iterlines()` is that this method is allowed to re-wrap the line if necessary. - :Parameters: - width : float or ``None`` - The width of the box in which the text will be fitted. Lines will - be wrapped if they are wider than this width. + @param width: The width of the box in which the text will be fitted. + Lines will be wrapped if they are wider than this width. """ ctx = self.context for line in self._text.split("\n"): diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 8d2fce8c4..2b1f26aa6 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -146,13 +146,13 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): Some simple examples: >>> from igraph import Graph - >>> print Graph.Formula() # empty graph + >>> print(Graph.Formula()) # empty graph IGRAPH UN-- 0 0 -- + attr: name (v) >>> g = Graph.Formula("A-B") # undirected graph >>> g.vs["name"] ['A', 'B'] - >>> print g + >>> print(g) IGRAPH UN-- 2 1 -- + attr: name (v) + edges (vertex names): @@ -165,7 +165,7 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): >>> g = Graph.Formula("A ---> B") # directed graph >>> g.vs["name"] ['A', 'B'] - >>> print g + >>> print(g) IGRAPH DN-- 2 1 -- + attr: name (v) + edges (vertex names): @@ -175,7 +175,7 @@ def construct_graph_from_formula(cls, formula=None, attr="name", simplify=True): with commas. You can also specify isolated vertices: >>> g = Graph.Formula("A--B, C--D, E--F, G--H, I, J, K") - >>> print ", ".join(g.vs["name"]) + >>> print(", ".join(g.vs["name"])) A, B, C, D, E, F, G, H, I, J, K >>> g.clusters().membership [0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 6] diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 1d3fd7623..7dd9292df 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -56,13 +56,13 @@ class Layout(object): >>> layout = Layout([(0, 1), (0, 2)]) >>> coords = layout[1] - >>> print coords + >>> print(coords) [0, 2] >>> coords = (0, 3) - >>> print layout[1] + >>> print(layout[1]) [0, 2] >>> layout[1] = coords - >>> print layout[1] + >>> print(layout[1]) [0, 3] """ diff --git a/src/igraph/operators.py b/src/igraph/operators.py index a8c19a40d..8fe113b5c 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -43,7 +43,7 @@ def disjoint_union(graphs): An error is generated if some input graphs are directed and others are undirected. - @param graph: list of graphs. A lazy sequence is not acceptable. + @param graphs: list of graphs. A lazy sequence is not acceptable. @return: the disjoint union graph """ if any(not isinstance(g, GraphBase) for g in graphs): @@ -120,7 +120,7 @@ def union(graphs, byname="auto"): An error is generated if some input graphs are directed and others are undirected. - @param graph: list of graphs. A lazy sequence is not acceptable. + @param graphs: list of graphs. A lazy sequence is not acceptable. @param byname: bool or 'auto' specifying the function behaviour with respect to names vertices (i.e. vertices with the 'name' attribute). If False, ignore vertex names. If True, merge vertices based on names. If @@ -297,7 +297,7 @@ def intersection(graphs, byname="auto", keep_all_vertices=True): An error is generated if some input graphs are directed and others are undirected. - @param graph: list of graphs. A lazy sequence is not acceptable. + @param graphs: list of graphs. A lazy sequence is not acceptable. @param byname: bool or 'auto' specifying the function behaviour with respect to names vertices (i.e. vertices with the 'name' attribute). If False, ignore vertex names. If True, merge vertices based on names. If diff --git a/src/igraph/remote/gephi.py b/src/igraph/remote/gephi.py index 5cda23739..74b9d0d46 100644 --- a/src/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -2,22 +2,9 @@ # -*- coding: utf-8 -*- """Classes that help igraph communicate with Gephi (http://www.gephi.org).""" -from igraph.compat import property +import json import urllib.request, urllib.error, urllib.parse -try: - # JSON is optional so we don't blow up with Python < 2.6 - import json -except ImportError: - try: - # Try with simplejson for Python < 2.6 - import simplejson as json - except ImportError: - # No simplejson either - from igraph.drawing.utils import FakeModule - - json = FakeModule() - __all__ = ("GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat") __docformat__ = "restructuredtext en" __license__ = """\ @@ -202,7 +189,7 @@ class GephiGraphStreamer(object): The Gephi graph streaming format is a simple JSON-based format that can be used to post mutations to a graph (i.e. node and edge additions, removals and updates) - to a remote component. For instance, one can open up Gephi (http://www.gephi.org}), + to a remote component. For instance, one can open up Gephi (http://www.gephi.org), install the Gephi graph streaming plugin and then send a graph from igraph straight into the Gephi window by using `GephiGraphStreamer` with the appropriate URL where Gephi is listening. @@ -215,7 +202,7 @@ class GephiGraphStreamer(object): >>> streamer = GephiGraphStreamer() >>> graph = Graph.Formula("A --> B, B --> C") >>> streamer.post(graph, buf) - >>> print buf.getvalue() # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE + >>> print(buf.getvalue()) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE {"an": {"igraph:...:v:0": {"name": "A"}}} {"an": {"igraph:...:v:1": {"name": "B"}}} {"an": {"igraph:...:v:2": {"name": "C"}}} diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index dda450851..85955d9f2 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -45,23 +45,23 @@ class FittedPowerLaw(object): >>> result = power_law_fit([1, 2, 3, 4, 5, 6]) >>> result # doctest:+ELLIPSIS - FittedPowerLaw(continuous=False, alpha=2.425828..., xmin=3.0, L=-7.54633..., D=0.2138..., p=0.99311...) + FittedPowerLaw(continuous=False, alpha=2.42..., xmin=3.0, L=-7.54..., D=0.21..., p=0.993...) >>> print(result) # doctest:+ELLIPSIS Fitted power-law distribution on discrete data - Exponent (alpha) = 2.425828 + Exponent (alpha) = 2.42... Cutoff (xmin) = 3.000000 - Log-likelihood = -7.546337 + Log-likelihood = -7.54... H0: data was drawn from the fitted distribution - KS test statistic = 0.213817 - p-value = 0.993111 + KS test statistic = 0.21... + p-value = 0.993... H0 could not be rejected at significance level 0.05 >>> result.alpha # doctest:+ELLIPSIS - 2.425828... + 2.42... >>> result.xmin 3.0 >>> result.continuous diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 6dbffe1c5..24fdf1113 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -1,9 +1,6 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- -"""Summary representation of a graph. - -@undocumented: _get_wrapper_for_width, FakeWrapper -""" +"""Summary representation of a graph.""" from igraph.statistics import median from itertools import islice @@ -83,11 +80,6 @@ class GraphSummary(object): Edges may be presented as an ordinary edge list or an adjacency list. By default, this depends on the number of edges; however, you can control it with the appropriate constructor arguments. - - @undocumented: _construct_edgelist_adjlist, _construct_edgelist_compressed, - _construct_edgelist_edgelist, _construct_graph_attributes, - _construct_vertex_attributes, _construct_header, _edge_attribute_iterator, - _infer_column_alignment, _new_table, _vertex_attribute_iterator """ def __init__( diff --git a/src/igraph/utils.py b/src/igraph/utils.py index 707afb0da..085f4a5ff 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -1,9 +1,6 @@ # vim:ts=4:sw=4:sts=4:et # -*- coding: utf-8 -*- -"""Utility functions that cannot be categorised anywhere else. - -@undocumented: _is_running_in_ipython -""" +"""Utility functions that cannot be categorised anywhere else.""" from contextlib import contextmanager From dffaedfab3707e982c4002f1636c64cc2a85b5e5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Feb 2021 13:25:12 +0100 Subject: [PATCH 0188/1681] doc: updated status badges in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 196de140f..33bd74214 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -[![Travis CI](https://img.shields.io/travis/igraph/python-igraph)](https://travis-ci.org/igraph/python-igraph) +[![Build and test with tox](https://github.com/igraph/python-igraph/actions/workflows/build.yml/badge.svg)](https://github.com/igraph/python-igraph/actions/workflows/build.yml) +[![Build status](https://ci.appveyor.com/api/projects/status/55i1d4g65q11f9l5?svg=true)](https://ci.appveyor.com/project/ntamas/python-igraph-jst2e) [![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://pypi.python.org/pypi/python-igraph) [![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) From ed39d05d10fbecfd0ab4eb2c7234287a5640e196 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Feb 2021 13:34:00 +0100 Subject: [PATCH 0189/1681] build: GMP also has to be vendored --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index da407363b..f68e0dc7c 100644 --- a/setup.py +++ b/setup.py @@ -248,7 +248,7 @@ def compile_in(self, source_folder, build_folder, install_folder): args.extend(shlex.split(os.environ["IGRAPH_CMAKE_EXTRA_ARGS"])) # Build the Python interface with vendored libraries - for deps in "ARPACK BLAS CXSPARSE GLPK LAPACK".split(): + for deps in "ARPACK BLAS CXSPARSE GLPK GMP LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") # -fPIC is needed on Linux so we can link to a static igraph lib from a From 82248f1e64aa6cd71b8cd1ecdc0c01dd2d047724 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Feb 2021 13:34:32 +0100 Subject: [PATCH 0190/1681] fix: extra CMake args in setup script should come after the default ones so they can be overridden --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index f68e0dc7c..88bd0d577 100644 --- a/setup.py +++ b/setup.py @@ -243,10 +243,6 @@ def compile_in(self, source_folder, build_folder, install_folder): print("Configuring build...") args = [cmake] - # Add any extra CMake args from environment variables - if "IGRAPH_CMAKE_EXTRA_ARGS" in os.environ: - args.extend(shlex.split(os.environ["IGRAPH_CMAKE_EXTRA_ARGS"])) - # Build the Python interface with vendored libraries for deps in "ARPACK BLAS CXSPARSE GLPK GMP LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") @@ -255,6 +251,10 @@ def compile_in(self, source_folder, build_folder, install_folder): # Python shared library args.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON") + # Add any extra CMake args from environment variables + if "IGRAPH_CMAKE_EXTRA_ARGS" in os.environ: + args.extend(shlex.split(os.environ["IGRAPH_CMAKE_EXTRA_ARGS"])) + # Finally, add the source folder path args.append(str(build_to_source_folder)) From 03640186d232192e458582a99c9010c8c6af70ad Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Feb 2021 14:00:51 +0100 Subject: [PATCH 0191/1681] doc: replaced autotools with CMake in README.md [ci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33bd74214..c65591029 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ code are picked up automatically by Python: python setup.py develop ``` -**NOTE**: Building requires `autotools`, a C compiler, and a few more dependencies. +**NOTE**: Building requires `CMake`, a C compiler, and a few more dependencies. Changes that you make to the Python code do not need any extra action. However, if you adjust the source code of the C extension, you need to rebuild it by running From f0c1cfa8ea0f1b2f0c6eac2661b3d3e048297796 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 1 Mar 2021 11:29:15 +0100 Subject: [PATCH 0192/1681] fix: nicer formatting of error messages when they do not end with a punctuation mark --- src/_igraph/error.c | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/_igraph/error.c b/src/_igraph/error.c index bb99960b8..2954ad7b7 100644 --- a/src/_igraph/error.c +++ b/src/_igraph/error.c @@ -41,11 +41,12 @@ PyObject* igraphmodule_InternalError; * \c NULL value to their callers until it is propagated to the Python * interpreter. */ -PyObject* igraphmodule_handle_igraph_error() -{ +PyObject* igraphmodule_handle_igraph_error() { if (!PyErr_Occurred()) { - PyErr_SetString(igraphmodule_InternalError, - "Internal igraph error. Please contact the author!"); + PyErr_SetString( + igraphmodule_InternalError, + "Internal igraph error. Please contact the author!" + ); } return NULL; @@ -56,7 +57,7 @@ PyObject* igraphmodule_handle_igraph_error() * \brief Warning hook for \c igraph */ void igraphmodule_igraph_warning_hook(const char *reason, const char *file, - int line, int igraph_errno) { + int line, int igraph_errno) { char buf[4096]; snprintf(buf, sizeof(buf), "%s at %s:%i", reason, file, line); PyErr_Warn(PyExc_RuntimeWarning, buf); @@ -67,18 +68,29 @@ void igraphmodule_igraph_warning_hook(const char *reason, const char *file, * \brief Error hook for \c igraph */ void igraphmodule_igraph_error_hook(const char *reason, const char *file, - int line, int igraph_errno) { + int line, int igraph_errno) { char buf[4096]; + char* punctuation = ""; PyObject *exc = igraphmodule_InternalError; if (igraph_errno == IGRAPH_UNIMPLEMENTED) - exc = PyExc_NotImplementedError; + exc = PyExc_NotImplementedError; if (igraph_errno == IGRAPH_ENOMEM) - exc = PyExc_MemoryError; + exc = PyExc_MemoryError; - snprintf(buf, sizeof(buf), "Error at %s:%i: %s, %s", file, line, reason, - igraph_strerror(igraph_errno)); + /* add a full stop at the end of the error message for nicer formatting */ + if (reason && strlen(reason) > 1) { + char last_char = reason[strlen(reason) - 1]; + if (last_char != '.' && last_char != '?' && last_char != '!') { + punctuation = "."; + } + } + + snprintf( + buf, sizeof(buf), "Error at %s:%i: %s%s -- %s", file, line, reason, + punctuation, igraph_strerror(igraph_errno) + ); IGRAPH_FINALLY_FREE(); /* make sure we are not masking already thrown exceptions */ From 01dc57b10a4a28f708d381db5be27b81af3b87df Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 5 Mar 2021 13:31:48 +0100 Subject: [PATCH 0193/1681] fix: removed all mentions of 'long', which is not present in Python 3 any more, fixes #372 --- src/igraph/clustering.py | 4 ++-- src/igraph/statistics.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 7380e7e03..4217f3dce 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -1535,7 +1535,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): first = None if first is not None: # Okay. Is the first element of the list a single number? - if isinstance(first, (int, long)): + if isinstance(first, int): # Yes. Seems like we have a list of cluster indices. # Assign color indices automatically. group_iter = ((group, color) for color, group in enumerate(mark_groups)) @@ -1552,7 +1552,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): def cluster_index_resolver(): for group, color in group_iter: - if isinstance(group, (int, long)): + if isinstance(group, int): group = clustering[group] yield group, color diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 85955d9f2..951db6d9c 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -474,9 +474,6 @@ def __float__(self): def __int__(self): return int(self._mean) - def __long__(self): - return int(self._mean) - def __complex__(self): return complex(self._mean) From c9b2ec21b0629ebabdf2739030464852f2689111 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 7 Mar 2021 01:34:31 +0100 Subject: [PATCH 0194/1681] doc: documentation improvements; using __text_signature__ for C functions, started migrating to PyDoctor --- MANIFEST.in | 3 - scripts/epydoc.cfg | 9 - scripts/mkdoc.sh | 47 ++- src/_igraph/edgeobject.c | 10 +- src/_igraph/edgeseqobject.c | 12 +- src/_igraph/graphobject.c | 549 +++++++++++++++++----------------- src/_igraph/igraphmodule.c | 32 +- src/_igraph/vertexobject.c | 6 +- src/_igraph/vertexseqobject.c | 11 +- src/igraph/__init__.py | 5 +- src/igraph/clustering.py | 10 - 11 files changed, 333 insertions(+), 361 deletions(-) delete mode 100644 scripts/epydoc.cfg diff --git a/MANIFEST.in b/MANIFEST.in index 9bfa4b090..bbfa86415 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,10 +1,7 @@ -include setup.cfg include src/_igraph/*.h include MANIFEST.in include COPYING include scripts/mkdoc.sh -include scripts/epydoc-patched -include scripts/epydoc.cfg include tests/*.py graft vendor/source/igraph diff --git a/scripts/epydoc.cfg b/scripts/epydoc.cfg deleted file mode 100644 index a474af3af..000000000 --- a/scripts/epydoc.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[epydoc] - -name: igraph library -url: http://igraph.org - -modules: igraph, igraph.app, igraph.app.shell, igraph.statistics -exclude: igraph.compat, igraph.formula, igraph.test, igraph.vendor - -imports: yes diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index ccbbd6bbf..214027c4d 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -1,6 +1,6 @@ #!/bin/sh # -# Creates documentation for igraph's Python interface using epydoc +# Creates the API documentation for igraph's Python interface using PyDoctor # # Usage: ./mkdoc.sh [--sync] [directory] @@ -9,41 +9,38 @@ SCRIPTS_FOLDER=`dirname $0` cd ${SCRIPTS_FOLDER}/.. ROOT_FOLDER=`pwd` DOC_API_FOLDER=${ROOT_FOLDER}/doc/api -CONFIG=${ROOT_FOLDER}/scripts/epydoc.cfg cd ${ROOT_FOLDER} -mkdir -p ${DOC_API_FOLDER}/pdf -mkdir -p ${DOC_API_FOLDER}/html -EPYDOC="${ROOT_FOLDER}/scripts/epydoc-patched" -python -m epydoc.__init__ -if [ $? -gt 0 ]; then - echo "Epydoc not installed, exiting..." +if [ ! -d ".venv" ]; then + # Create a virtual environment for pydoctor + python3 -m venv .venv + .venv/bin/pip install pydoctor +fi + +PYDOCTOR=.venv/bin/pydoctor +if [ ! -f ${PYDOCTOR} ]; then + echo "PyDoctor not installed in the virtualenv of the project, exiting..." exit 1 fi PWD=`pwd` -echo "Checking symlinked _igraph.so in ${ROOT_FOLDER}/src/igraph..." -if [ ! -e ${ROOT_FOLDER}/src/igraph/_igraph.so -o ! -L ${ROOT_FOLDER}/src/igraph/_igraph.so ]; then - rm -f ${ROOT_FOLDER}/src/igraph/_igraph.so - cd ${ROOT_FOLDER}/src/igraph - ln -s ../../build/lib*/igraph/_igraph.so . - cd ${ROOT_FOLDER} -fi - echo "Removing existing documentation..." -rm -rf html +rm -rf "${DOC_API_FOLDER}/html" "${DOC_API_FOLDER}/pdf" -echo "Generating HTML documentation..." -PYTHONPATH=src ${EPYDOC} --html -v -o ${DOC_API_FOLDER}/html --config ${CONFIG} +IGRAPHDIR=`.venv/bin/python3 -c 'import igraph, os; print(os.path.dirname(igraph.__file__))'` -PDF=0 -which latex >/dev/null && PDF=1 +echo "Generating HTML documentation..." +"$PYDOCTOR" \ + --project-name "python-igraph" \ + --project-url "https://igraph.org/python" \ + --introspect-c-modules \ + --make-html \ + --html-output "${DOC_API_FOLDER}/html" \ + ${IGRAPHDIR} -if [ $PDF -eq 1 ]; then - echo "Generating PDF documentation..." - PYTHONPATH=src ${EPYDOC} --pdf -v -o ${DOC_API_FOLDER}/pdf --config ${CONFIG} -fi +# PDF not supported by PyDoctor cd "$PWD" + diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index d438e9aa7..253f169b5 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -631,17 +631,17 @@ GRAPH_PROXY_METHOD(is_mutual, "is_mutual"); PyMethodDef igraphmodule_Edge_methods[] = { {"attributes", (PyCFunction)igraphmodule_Edge_attributes, METH_NOARGS, - "attributes() -> dict\n\n" + "attributes() -> dict\n--\n\n" "Returns a dict of attribute names and values for the edge\n" }, {"attribute_names", (PyCFunction)igraphmodule_Edge_attribute_names, - METH_NOARGS, - "attribute_names() -> list\n\n" - "Returns the list of edge attribute names\n" + METH_NOARGS, + "attribute_names() -> list\n--\n\n" + "Returns the list of edge attribute names\n" }, {"update_attributes", (PyCFunction)igraphmodule_Edge_update_attributes, METH_VARARGS | METH_KEYWORDS, - "update_attributes(E, **F) -> None\n\n" + "update_attributes(E, **F) -> None\n--\n\n" "Updates the attributes of the edge from dict/iterable E and F.\n\n" "If E has a C{keys()} method, it does: C{for k in E: self[k] = E[k]}.\n" "If E lacks a C{keys()} method, it does: C{for (k, v) in E: self[k] = v}.\n" diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index ee202203e..fc7af6629 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -797,36 +797,36 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject PyMethodDef igraphmodule_EdgeSeq_methods[] = { {"attribute_names", (PyCFunction)igraphmodule_EdgeSeq_attribute_names, METH_NOARGS, - "attribute_names() -> list\n\n" + "attribute_names() -> list\n--\n\n" "Returns the attribute name list of the graph's edges\n" }, {"find", (PyCFunction)igraphmodule_EdgeSeq_find, METH_VARARGS, - "find(condition) -> Edge\n\n" + "find(condition) -> Edge\n--\n\n" "For internal use only.\n" }, {"get_attribute_values", (PyCFunction)igraphmodule_EdgeSeq_get_attribute_values, METH_O, - "get_attribute_values(attrname) -> list\n\n" + "get_attribute_values(attrname) -> list\n--\n\n" "Returns the value of a given edge attribute for all edges.\n\n" "@param attrname: the name of the attribute\n" }, {"is_all", (PyCFunction)igraphmodule_EdgeSeq_is_all, METH_NOARGS, - "is_all() -> bool\n\n" + "is_all() -> bool\n--\n\n" "Returns whether the edge sequence contains all the edges exactly once, in\n" "the order of their edge IDs.\n\n" "This is used for optimizations in some of the edge selector routines.\n" }, {"set_attribute_values", (PyCFunction)igraphmodule_EdgeSeq_set_attribute_values, METH_VARARGS | METH_KEYWORDS, - "set_attribute_values(attrname, values) -> list\n" + "set_attribute_values(attrname, values) -> list\n--\n\n" "Sets the value of a given edge attribute for all vertices\n" "@param attrname: the name of the attribute\n" "@param values: the new attribute values in a list\n" }, {"select", (PyCFunction)igraphmodule_EdgeSeq_select, METH_VARARGS, - "select(...) -> VertexSeq\n\n" + "select(...) -> VertexSeq\n--\n\n" "For internal use only.\n" }, {NULL} diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 06dab05ac..e979b394a 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -280,7 +280,7 @@ PyObject* igraphmodule_Graph_subclass_from_igraph_t( PyObject* kwds; if (!PyType_IsSubtype(type, &igraphmodule_GraphType)) { - PyErr_SetString(PyExc_TypeError, "igraph.GraphBase expected"); + PyErr_SetString(PyExc_TypeError, "igraph._igraph.GraphBase expected"); return 0; } @@ -11998,21 +11998,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_vcount {"vcount", (PyCFunction) igraphmodule_Graph_vcount, METH_NOARGS, - "vcount()\n\n" + "vcount() -> int\n--\n\n" "Counts the number of vertices.\n" "@return: the number of vertices in the graph.\n" "@rtype: integer"}, // interface to igraph_ecount {"ecount", (PyCFunction) igraphmodule_Graph_ecount, METH_NOARGS, - "ecount()\n\n" + "ecount() -> int\n--\n\n" "Counts the number of edges.\n" "@return: the number of edges in the graph.\n" "@rtype: integer"}, // interface to igraph_is_dag {"is_dag", (PyCFunction) igraphmodule_Graph_is_dag, METH_NOARGS, - "is_dag()\n\n" + "is_dag() -> bool\n--\n\n" "Checks whether the graph is a DAG (directed acyclic graph).\n\n" "A DAG is a directed graph with no directed cycles.\n\n" "@return: C{True} if it is a DAG, C{False} otherwise.\n" @@ -12021,7 +12021,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_is_directed {"is_directed", (PyCFunction) igraphmodule_Graph_is_directed, METH_NOARGS, - "is_directed()\n\n" + "is_directed() -> bool\n--\n\n" "Checks whether the graph is directed.\n" "@return: C{True} if it is directed, C{False} otherwise.\n" "@rtype: boolean"}, @@ -12029,7 +12029,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_is_simple {"is_simple", (PyCFunction) igraphmodule_Graph_is_simple, METH_NOARGS, - "is_simple()\n\n" + "is_simple() -> bool\n--\n\n" "Checks whether the graph is simple (no loop or multiple edges).\n\n" "@return: C{True} if it is simple, C{False} otherwise.\n" "@rtype: boolean"}, @@ -12037,14 +12037,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_add_vertices */ {"add_vertices", (PyCFunction) igraphmodule_Graph_add_vertices, METH_VARARGS, - "add_vertices(n)\n\n" + "add_vertices(n: int) -> None\n--\n\n" "Adds vertices to the graph.\n\n" "@param n: the number of vertices to be added\n"}, /* interface to igraph_delete_vertices */ {"delete_vertices", (PyCFunction) igraphmodule_Graph_delete_vertices, METH_VARARGS, - "delete_vertices(vs)\n\n" + "delete_vertices(vs) -> None\n--\n\n" "Deletes vertices and all its edges from the graph.\n\n" "@param vs: a single vertex ID or the list of vertex IDs\n" " to be deleted. No argument deletes all vertices.\n"}, @@ -12052,7 +12052,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_add_edges */ {"add_edges", (PyCFunction) igraphmodule_Graph_add_edges, METH_VARARGS, - "add_edges(es)\n\n" + "add_edges(es) -> None\n--\n\n" "Adds edges to the graph.\n\n" "@param es: the list of edges to be added. Every edge is\n" " represented with a tuple, containing the vertex IDs of the\n" @@ -12061,7 +12061,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_delete_edges */ {"delete_edges", (PyCFunction) igraphmodule_Graph_delete_edges, METH_VARARGS | METH_KEYWORDS, - "delete_edges(es)\n\n" + "delete_edges(es) -> None\n--\n\n" "Removes edges from the graph.\n\n" "All vertices will be kept, even if they lose all their edges.\n" "Nonexistent edges will be silently ignored.\n\n" @@ -12072,7 +12072,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_degree */ {"degree", (PyCFunction) igraphmodule_Graph_degree, METH_VARARGS | METH_KEYWORDS, - "degree(vertices, mode=ALL, loops=True)\n\n" + "degree(vertices, mode=ALL, loops=True)\n--\n\n" "Returns some vertex degrees from the graph.\n\n" "This method accepts a single vertex ID or a list of vertex IDs as a\n" "parameter, and returns the degree of the given vertices (in the\n" @@ -12087,7 +12087,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_strength */ {"strength", (PyCFunction) igraphmodule_Graph_strength, METH_VARARGS | METH_KEYWORDS, - "strength(vertices, mode=ALL, loops=True, weights=None)\n\n" + "strength(vertices, mode=ALL, loops=True, weights=None)\n--\n\n" "Returns the strength (weighted degree) of some vertices from the graph\n\n" "This method accepts a single vertex ID or a list of vertex IDs as a\n" "parameter, and returns the strength (that is, the sum of the weights\n" @@ -12108,7 +12108,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_loop */ {"is_loop", (PyCFunction) igraphmodule_Graph_is_loop, METH_VARARGS | METH_KEYWORDS, - "is_loop(edges=None)\n\n" + "is_loop(edges=None) -> List[bool]\n--\n\n" "Checks whether a specific set of edges contain loop edges\n\n" "@param edges: edge indices which we want to check. If C{None}, all\n" " edges are checked.\n" @@ -12117,7 +12117,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_multiple */ {"is_multiple", (PyCFunction) igraphmodule_Graph_is_multiple, METH_VARARGS | METH_KEYWORDS, - "is_multiple(edges=None)\n\n" + "is_multiple(edges=None) -> List[bool]\n--\n\n" "Checks whether an edge is a multiple edge.\n\n" "Also works for a set of edges -- in this case, every edge is checked\n" "one by one. Note that if there are multiple edges going between a\n" @@ -12132,7 +12132,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_has_multiple */ {"has_multiple", (PyCFunction) igraphmodule_Graph_has_multiple, METH_NOARGS, - "has_multiple()\n\n" + "has_multiple() -> bool\n--\n\n" "Checks whether the graph has multiple edges.\n\n" "@return: C{True} if the graph has at least one multiple edge,\n" " C{False} otherwise.\n" @@ -12141,7 +12141,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_mutual */ {"is_mutual", (PyCFunction) igraphmodule_Graph_is_mutual, METH_VARARGS | METH_KEYWORDS, - "is_mutual(edges=None)\n\n" + "is_mutual(edges=None) -> Lis[bool]\n--\n\n" "Checks whether an edge has an opposite pair.\n\n" "Also works for a set of edges -- in this case, every edge is checked\n" "one by one. The result will be a list of booleans (or a single boolean\n" @@ -12160,7 +12160,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_count_multiple */ {"count_multiple", (PyCFunction) igraphmodule_Graph_count_multiple, METH_VARARGS | METH_KEYWORDS, - "count_multiple(edges=None)\n\n" + "count_multiple(edges=None) -> List[int]\n--\n\n" "Counts the multiplicities of the given edges.\n\n" "@param edges: edge indices for which we want to count their\n" " multiplicity. If C{None}, all edges are counted.\n" @@ -12169,7 +12169,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighbors */ {"neighbors", (PyCFunction) igraphmodule_Graph_neighbors, METH_VARARGS | METH_KEYWORDS, - "neighbors(vertex, mode=ALL)\n\n" + "neighbors(vertex, mode=ALL)\n--\n\n" "Returns adjacent vertices to a given vertex.\n\n" "@param vertex: a vertex ID\n" "@param mode: whether to return only successors (L{OUT}),\n" @@ -12178,20 +12178,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"successors", (PyCFunction) igraphmodule_Graph_successors, METH_VARARGS | METH_KEYWORDS, - "successors(vertex)\n\n" + "successors(vertex)\n--\n\n" "Returns the successors of a given vertex.\n\n" "Equivalent to calling the L{Graph.neighbors} method with type=L{OUT}."}, {"predecessors", (PyCFunction) igraphmodule_Graph_predecessors, METH_VARARGS | METH_KEYWORDS, - "predecessors(vertex)\n\n" + "predecessors(vertex)\n--\n\n" "Returns the predecessors of a given vertex.\n\n" "Equivalent to calling the L{Graph.neighbors} method with type=L{IN}."}, /* interface to igraph_get_eid */ {"get_eid", (PyCFunction) igraphmodule_Graph_get_eid, METH_VARARGS | METH_KEYWORDS, - "get_eid(v1, v2, directed=True, error=True)\n\n" + "get_eid(v1, v2, directed=True, error=True) -> int\n--\n\n" "Returns the edge ID of an arbitrary edge between vertices v1 and v2\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" @@ -12206,7 +12206,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_get_eids */ {"get_eids", (PyCFunction) igraphmodule_Graph_get_eids, METH_VARARGS | METH_KEYWORDS, - "get_eids(pairs=None, path=None, directed=True, error=True)\n\n" + "get_eids(pairs=None, path=None, directed=True, error=True) -> List[int]\n--\n\n" "Returns the edge IDs of some edges between some vertices.\n\n" "This method can operate in two different modes, depending on which\n" "of the keyword arguments C{pairs} and C{path} are given.\n\n" @@ -12234,7 +12234,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_incident */ {"incident", (PyCFunction) igraphmodule_Graph_incident, METH_VARARGS | METH_KEYWORDS, - "incident(vertex, mode=OUT)\n\n" + "incident(vertex, mode=OUT)\n--\n\n" "Returns the edges a given vertex is incident on.\n\n" "@param vertex: a vertex ID\n" "@param mode: whether to return only successors (L{OUT}),\n" @@ -12248,7 +12248,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_adjacency */ {"Adjacency", (PyCFunction) igraphmodule_Graph_Adjacency, METH_CLASS | METH_VARARGS | METH_KEYWORDS, - "Adjacency(matrix, mode=ADJ_DIRECTED)\n\n" + "Adjacency(matrix, mode=ADJ_DIRECTED)\n--\n\n" "Generates a graph from its adjacency matrix.\n\n" "@param matrix: the adjacency matrix\n" "@param mode: the mode to be used. Possible values are:\n" @@ -12272,7 +12272,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"Asymmetric_Preference", (PyCFunction) igraphmodule_Graph_Asymmetric_Preference, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Asymmetric_Preference(n, type_dist_matrix, pref_matrix, attribute=None, loops=False)\n\n" + "Asymmetric_Preference(n, type_dist_matrix, pref_matrix, attribute=None, loops=False)\n--\n\n" "Generates a graph based on asymmetric vertex types and connection probabilities.\n\n" "This is the asymmetric variant of L{Graph.Preference}.\n" "A given number of vertices are generated. Every vertex is assigned to an\n" @@ -12293,7 +12293,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_atlas {"Atlas", (PyCFunction) igraphmodule_Graph_Atlas, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Atlas(idx)\n\n" + "Atlas(idx: int)\n--\n\n" "Generates a graph from the Graph Atlas.\n\n" "@param idx: The index of the graph to be generated.\n" " Indices start from zero, graphs are listed:\n\n" @@ -12311,7 +12311,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"Barabasi", (PyCFunction) igraphmodule_Graph_Barabasi, METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Barabasi(n, m, outpref=False, directed=False, power=1,\n" - " zero_appeal=1, implementation=\"psumtree\", start_from=None)\n\n" + " zero_appeal=1, implementation=\"psumtree\", start_from=None)\n--\n\n" "Generates a graph based on the Barabasi-Albert model.\n\n" "@param n: the number of vertices\n" "@param m: either the number of outgoing edges generated for\n" @@ -12353,14 +12353,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_create_bipartite */ {"_Bipartite", (PyCFunction) igraphmodule_Graph_Bipartite, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Bipartite(types, edges, directed=False)\n\n" + "_Bipartite(types, edges, directed=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Bipartite()\n\n"}, /* interface to igraph_de_bruijn */ {"De_Bruijn", (PyCFunction) igraphmodule_Graph_De_Bruijn, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "De_Bruijn(m, n)\n\n" + "De_Bruijn(m, n)\n--\n\n" "Generates a de Bruijn graph with parameters (m, n)\n\n" "A de Bruijn graph represents relationships between strings. An alphabet\n" "of M{m} letters are used and strings of length M{n} are considered.\n" @@ -12377,7 +12377,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_establishment_game {"Establishment", (PyCFunction) igraphmodule_Graph_Establishment, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Establishment(n, k, type_dist, pref_matrix, directed=False)\n\n" + "Establishment(n, k, type_dist, pref_matrix, directed=False)\n--\n\n" "Generates a graph based on a simple growing model with vertex types.\n\n" "A single vertex is added at each time step. This new vertex tries to\n" "connect to k vertices in the graph. The probability that such a\n" @@ -12393,7 +12393,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_erdos_renyi_game {"Erdos_Renyi", (PyCFunction) igraphmodule_Graph_Erdos_Renyi, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Erdos_Renyi(n, p, m, directed=False, loops=False)\n\n" + "Erdos_Renyi(n, p, m, directed=False, loops=False)\n--\n\n" "Generates a graph based on the Erdos-Renyi model.\n\n" "@param n: the number of vertices.\n" "@param p: the probability of edges. If given, C{m} must be missing.\n" @@ -12404,7 +12404,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_famous */ {"Famous", (PyCFunction) igraphmodule_Graph_Famous, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Famous(name)\n\n" + "Famous(name)\n--\n\n" "Generates a famous graph based on its name.\n\n" "Several famous graphs are known to C{igraph} including (but not limited to)\n" "the Chvatal graph, the Petersen graph or the Tutte graph. This method\n" @@ -12417,7 +12417,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_forest_fire_game */ {"Forest_Fire", (PyCFunction) igraphmodule_Graph_Forest_Fire, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Forest_Fire(n, fw_prob, bw_factor=0.0, ambs=1, directed=False)\n\n" + "Forest_Fire(n, fw_prob, bw_factor=0.0, ambs=1, directed=False)\n--\n\n" "Generates a graph based on the forest fire model\n\n" "The forest fire model is a growing graph model. In every time step, a new\n" "vertex is added to the graph. The new vertex chooses an ambassador (or\n" @@ -12437,7 +12437,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_full_citation */ {"Full_Citation", (PyCFunction) igraphmodule_Graph_Full_Citation, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Full_Citation(n, directed=False)\n\n" + "Full_Citation(n, directed=False)\n--\n\n" "Generates a full citation graph\n\n" "A full citation graph is a graph where the vertices are indexed from 0 to\n" "M{n-1} and vertex M{i} has a directed edge towards all vertices with an\n" @@ -12448,7 +12448,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_full */ {"Full", (PyCFunction) igraphmodule_Graph_Full, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Full(n, directed=False, loops=False)\n\n" + "Full(n, directed=False, loops=False)\n--\n\n" "Generates a full graph (directed or undirected, with or without loops).\n\n" "@param n: the number of vertices.\n" "@param directed: whether to generate a directed graph.\n" @@ -12457,21 +12457,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_full_bipartite */ {"_Full_Bipartite", (PyCFunction) igraphmodule_Graph_Full_Bipartite, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Full_Bipartite(n1, n2, directed=False, loops=False)\n\n" + "_Full_Bipartite(n1, n2, directed=False, loops=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Full_Bipartite()\n\n"}, /* interface to igraph_grg_game */ {"_GRG", (PyCFunction) igraphmodule_Graph_GRG, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_GRG(n, radius, torus=False)\n\n" + "_GRG(n, radius, torus=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.GRG()\n\n"}, /* interface to igraph_growing_random_game */ {"Growing_Random", (PyCFunction) igraphmodule_Graph_Growing_Random, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Growing_Random(n, m, directed=False, citation=False)\n\n" + "Growing_Random(n, m, directed=False, citation=False)\n--\n\n" "Generates a growing random graph.\n\n" "@param n: The number of vertices in the graph\n" "@param m: The number of edges to add in each step (after adding a new vertex)\n" @@ -12482,14 +12482,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_incidence */ {"_Incidence", (PyCFunction) igraphmodule_Graph_Incidence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Incidence(matrix, directed=False, mode=ALL, multiple=False)\n\n" + "_Incidence(matrix, directed=False, mode=ALL, multiple=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Incidence()\n\n"}, /* interface to igraph_kautz */ {"Kautz", (PyCFunction) igraphmodule_Graph_Kautz, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Kautz(m, n)\n\n" + "Kautz(m, n)\n--\n\n" "Generates a Kautz graph with parameters (m, n)\n\n" "A Kautz graph is a labeled graph, vertices are labeled by strings\n" "of length M{n+1} above an alphabet with M{m+1} letters, with\n" @@ -12505,7 +12505,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_k_regular */ {"K_Regular", (PyCFunction) igraphmodule_Graph_K_Regular, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "K_Regular(n, k, directed=False, multiple=False)\n\n" + "K_Regular(n, k, directed=False, multiple=False)\n--\n\n" "Generates a k-regular random graph\n\n" "A k-regular random graph is a random graph where each vertex has degree k.\n" "If the graph is directed, both the in-degree and the out-degree of each vertex\n" @@ -12520,7 +12520,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_preference_game */ {"Preference", (PyCFunction) igraphmodule_Graph_Preference, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Preference(n, type_dist, pref_matrix, attribute=None, directed=False, loops=False)\n\n" + "Preference(n, type_dist, pref_matrix, attribute=None, directed=False, loops=False)\n--\n\n" "Generates a graph based on vertex types and connection probabilities.\n\n" "This is practically the nongrowing variant of L{Graph.Establishment}.\n" "A given number of vertices are generated. Every vertex is assigned to a\n" @@ -12539,14 +12539,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_bipartite_game */ {"_Random_Bipartite", (PyCFunction) igraphmodule_Graph_Random_Bipartite, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=\"all\")\n\n" + "_Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=\"all\")\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Random_Bipartite()\n\n"}, /* interface to igraph_recent_degree_game */ {"Recent_Degree", (PyCFunction) igraphmodule_Graph_Recent_Degree, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Recent_Degree(n, m, window, outpref=False, directed=False, power=1)\n\n" + "Recent_Degree(n, m, window, outpref=False, directed=False, power=1)\n--\n\n" "Generates a graph based on a stochastic model where the probability\n" "of an edge gaining a new node is proportional to the edges gained in\n" "a given time window.\n\n" @@ -12567,7 +12567,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_sbm_game */ {"SBM", (PyCFunction) igraphmodule_Graph_SBM, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "SBM(n, pref_matrix, block_sizes, directed=False, loops=False)\n\n" + "SBM(n, pref_matrix, block_sizes, directed=False, loops=False)\n--\n\n" "Generates a graph based on a stochastic blockmodel.\n\n" "A given number of vertices are generated. Every vertex is assigned to a\n" "vertex type according to the given block sizes. Vertices of the same\n" @@ -12586,7 +12586,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_star {"Star", (PyCFunction) igraphmodule_Graph_Star, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Star(n, mode=\"undirected\", center=0)\n\n" + "Star(n, mode=\"undirected\", center=0)\n--\n\n" "Generates a star graph.\n\n" "@param n: the number of vertices in the graph\n" "@param mode: Gives the type of the star graph to create. Should be\n" @@ -12596,7 +12596,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_lattice {"Lattice", (PyCFunction) igraphmodule_Graph_Lattice, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Lattice(dim, nei=1, directed=False, mutual=True, circular=True)\n\n" + "Lattice(dim, nei=1, directed=False, mutual=True, circular=True)\n--\n\n" "Generates a regular lattice.\n\n" "@param dim: list with the dimensions of the lattice\n" "@param nei: value giving the distance (number of steps) within which\n" @@ -12607,25 +12607,25 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param circular: whether the generated lattice is periodic.\n"}, /* interface to igraph_lcf */ - {"LCF", (PyCFunction) igraphmodule_Graph_LCF, - METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "LCF(n, shifts, repeats)\n\n" - "Generates a graph from LCF notation.\n\n" - "LCF is short for Lederberg-Coxeter-Frucht, it is a concise notation\n" - "for 3-regular Hamiltonian graphs. It consists of three parameters,\n" - "the number of vertices in the graph, a list of shifts giving\n" - "additional edges to a cycle backbone and another integer giving how\n" - "many times the shifts should be performed. See\n" - "U{http://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" - "@param n: the number of vertices\n" - "@param shifts: the shifts in a list or tuple\n" - "@param repeats: the number of repeats\n" - }, + {"LCF", (PyCFunction) igraphmodule_Graph_LCF, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "LCF(n, shifts, repeats)\n--\n\n" + "Generates a graph from LCF notation.\n\n" + "LCF is short for Lederberg-Coxeter-Frucht, it is a concise notation\n" + "for 3-regular Hamiltonian graphs. It consists of three parameters,\n" + "the number of vertices in the graph, a list of shifts giving\n" + "additional edges to a cycle backbone and another integer giving how\n" + "many times the shifts should be performed. See\n" + "U{http://mathworld.wolfram.com/LCFNotation.html} for details.\n\n" + "@param n: the number of vertices\n" + "@param shifts: the shifts in a list or tuple\n" + "@param repeats: the number of repeats\n" + }, // interface to igraph_ring {"Ring", (PyCFunction) igraphmodule_Graph_Ring, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Ring(n, directed=False, mutual=False, circular=True)\n\n" + "Ring(n, directed=False, mutual=False, circular=True)\n--\n\n" "Generates a ring graph.\n\n" "@param n: the number of vertices in the ring\n" "@param directed: whether to create a directed ring.\n" @@ -12635,7 +12635,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_static_fitness_game */ {"Static_Fitness", (PyCFunction) igraphmodule_Graph_Static_Fitness, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Static_Fitness(m, fitness_out, fitness_in=None, loops=False, multiple=False)\n\n" + "Static_Fitness(m, fitness_out, fitness_in=None, loops=False, multiple=False)\n--\n\n" "Generates a non-growing graph with edge probabilities proportional to node\n" "fitnesses.\n\n" "The algorithm randomly selects vertex pairs and connects them until the given\n" @@ -12659,8 +12659,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_static_power_law_game */ {"Static_Power_Law", (PyCFunction) igraphmodule_Graph_Static_Power_Law, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Static_Power_Law(n, m, exponent_out, exponent_in=-1, loops=False,\n" - " multiple=False, finite_size_correction=True)\n\n" + "Static_Power_Law(n, m, exponent_out, exponent_in=-1, loops=False, " + "multiple=False, finite_size_correction=True)\n--\n\n" "Generates a non-growing graph with prescribed power-law degree distributions.\n\n" "@param n: the number of vertices in the graph\n" "@param m: the number of edges in the graph\n" @@ -12691,7 +12691,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_tree {"Tree", (PyCFunction) igraphmodule_Graph_Tree, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Tree(n, children, type=TREE_UNDIRECTED)\n\n" + "Tree(n, children, type=TREE_UNDIRECTED)\n--\n\n" "Generates a tree in which almost all vertices have the same number of children.\n\n" "@param n: the number of vertices in the graph\n" "@param children: the number of children of a vertex in the graph\n" @@ -12702,7 +12702,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_degree_sequence_game */ {"Degree_Sequence", (PyCFunction) igraphmodule_Graph_Degree_Sequence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Degree_Sequence(out, in=None, method=\"simple\")\n\n" + "Degree_Sequence(out, in=None, method=\"simple\")\n--\n\n" "Generates a graph with a given degree sequence.\n\n" "@param out: the out-degree sequence for a directed graph. If the\n" " in-degree sequence is omitted, the generated graph\n" @@ -12736,7 +12736,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_isoclass_create */ {"Isoclass", (PyCFunction) igraphmodule_Graph_Isoclass, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Isoclass(n, class, directed=False)\n\n" + "Isoclass(n, class, directed=False)\n--\n\n" "Generates a graph with a given isomorphism class.\n\n" "@param n: the number of vertices in the graph (3 or 4)\n" "@param class: the isomorphism class\n" @@ -12745,7 +12745,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_watts_strogatz_game */ {"Watts_Strogatz", (PyCFunction) igraphmodule_Graph_Watts_Strogatz, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Watts_Strogatz(dim, size, nei, p, loops=False, multiple=False)\n\n" + "Watts_Strogatz(dim, size, nei, p, loops=False, multiple=False)\n--\n\n" "@param dim: the dimension of the lattice\n" "@param size: the size of the lattice along all dimensions\n" "@param nei: value giving the distance (number of steps) within which\n" @@ -12762,7 +12762,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_weighted_adjacency */ {"Weighted_Adjacency", (PyCFunction) igraphmodule_Graph_Weighted_Adjacency, METH_CLASS | METH_VARARGS | METH_KEYWORDS, - "Weighted_Adjacency(matrix, mode=ADJ_DIRECTED, attr=\"weight\", loops=True)\n\n" + "Weighted_Adjacency(matrix, mode=ADJ_DIRECTED, attr=\"weight\", loops=True)\n--\n\n" "Generates a graph from its adjacency matrix.\n\n" "@param matrix: the adjacency matrix\n" "@param mode: the mode to be used. Possible values are:\n" @@ -12793,7 +12793,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_are_connected {"are_connected", (PyCFunction) igraphmodule_Graph_are_connected, METH_VARARGS | METH_KEYWORDS, - "are_connected(v1, v2)\n\n" + "are_connected(v1, v2) -> bool\n--\n\n" "Decides whether two given vertices are directly connected.\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" @@ -12803,7 +12803,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_articulation_points */ {"articulation_points", (PyCFunction)igraphmodule_Graph_articulation_points, METH_NOARGS, - "articulation_points()\n\n" + "articulation_points()\n--\n\n" "Returns the list of articulation points in the graph.\n\n" "A vertex is an articulation point if its removal increases the number of\n" "connected components in the graph.\n" @@ -12812,7 +12812,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity */ {"assortativity", (PyCFunction)igraphmodule_Graph_assortativity, METH_VARARGS | METH_KEYWORDS, - "assortativity(types1, types2=None, directed=True)\n\n" + "assortativity(types1, types2=None, directed=True)\n--\n\n" "Returns the assortativity of the graph based on numeric properties\n" "of the vertices.\n\n" "This coefficient is basically the correlation between the actual\n" @@ -12842,7 +12842,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity_degree */ {"assortativity_degree", (PyCFunction)igraphmodule_Graph_assortativity_degree, METH_VARARGS | METH_KEYWORDS, - "assortativity_degree(directed=True)\n\n" + "assortativity_degree(directed=True)\n--\n\n" "Returns the assortativity of a graph based on vertex degrees.\n\n" "See L{assortativity()} for the details. L{assortativity_degree()} simply\n" "calls L{assortativity()} with the vertex degrees as types.\n\n" @@ -12855,7 +12855,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_assortativity_nominal */ {"assortativity_nominal", (PyCFunction)igraphmodule_Graph_assortativity_nominal, METH_VARARGS | METH_KEYWORDS, - "assortativity_nominal(types, directed=True)\n\n" + "assortativity_nominal(types, directed=True)\n--\n\n" "Returns the assortativity of the graph based on vertex categories.\n\n" "Assuming that the vertices belong to different categories, this\n" "function calculates the assortativity coefficient, which specifies\n" @@ -12878,7 +12878,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"average_path_length", (PyCFunction) igraphmodule_Graph_average_path_length, METH_VARARGS | METH_KEYWORDS, - "average_path_length(directed=True, unconn=True)\n\n" + "average_path_length(directed=True, unconn=True)\n--\n\n" "Calculates the average path length in a graph.\n\n" "@param directed: whether to consider directed paths in case of a\n" " directed graph. Ignored for undirected graphs.\n" @@ -12891,7 +12891,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_authority_score */ {"authority_score", (PyCFunction)igraphmodule_Graph_authority_score, METH_VARARGS | METH_KEYWORDS, - "authority_score(weights=None, scale=True, arpack_options=None, return_eigenvalue=False)\n\n" + "authority_score(weights=None, scale=True, arpack_options=None, return_eigenvalue=False)\n--\n\n" "Calculates Kleinberg's authority score for the vertices of the graph\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" @@ -12909,7 +12909,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_betweenness[_estimate] */ {"betweenness", (PyCFunction) igraphmodule_Graph_betweenness, METH_VARARGS | METH_KEYWORDS, - "betweenness(vertices=None, directed=True, cutoff=None, weights=None)\n\n" + "betweenness(vertices=None, directed=True, cutoff=None, weights=None)\n--\n\n" "Calculates or estimates the betweenness of vertices in a graph.\n\n" "Keyword arguments:\n" "@param vertices: the vertices for which the betweennesses must be returned.\n" @@ -12926,7 +12926,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to biconnected_components */ {"biconnected_components", (PyCFunction) igraphmodule_Graph_biconnected_components, METH_VARARGS | METH_KEYWORDS, - "biconnected_components(return_articulation_points=True)\n\n" + "biconnected_components(return_articulation_points=True)\n--\n\n" "Calculates the biconnected components of the graph.\n\n" "Components containing a single vertex only are not considered as being\n" "biconnected.\n\n" @@ -12940,21 +12940,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_bipartite_projection */ {"bipartite_projection", (PyCFunction) igraphmodule_Graph_bipartite_projection, METH_VARARGS | METH_KEYWORDS, - "bipartite_projection(types, multiplicity=True, probe1=-1, which=-1)\n\n" + "bipartite_projection(types, multiplicity=True, probe1=-1, which=-1)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.bipartite_projection()\n"}, /* interface to igraph_bipartite_projection_size */ {"bipartite_projection_size", (PyCFunction) igraphmodule_Graph_bipartite_projection_size, METH_VARARGS | METH_KEYWORDS, - "bipartite_projection_size(types)\n\n" + "bipartite_projection_size(types)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.bipartite_projection_size()\n"}, /* interface to igraph_bridges */ {"bridges", (PyCFunction)igraphmodule_Graph_bridges, METH_NOARGS, - "bridges()\n\n" + "bridges()\n--\n\n" "Returns the list of bridges in the graph.\n\n" "An edge is a bridge if its removal increases the number of (weakly) connected\n" "components in the graph.\n" @@ -12963,8 +12963,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_closeness */ {"closeness", (PyCFunction) igraphmodule_Graph_closeness, METH_VARARGS | METH_KEYWORDS, - "closeness(vertices=None, mode=ALL, cutoff=None, weights=None,\n" - " normalized=True)\n\n" + "closeness(vertices=None, mode=ALL, cutoff=None, weights=None, " + "normalized=True)\n--\n\n" "Calculates the closeness centralities of given vertices in a graph.\n\n" "The closeness centerality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" @@ -12996,8 +12996,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_harmonic_centrality */ {"harmonic_centrality", (PyCFunction) igraphmodule_Graph_harmonic_centrality, METH_VARARGS | METH_KEYWORDS, - "harmonic_centrality(vertices=None, mode=ALL, cutoff=None, weights=None,\n" - " normalized=True)\n\n" + "harmonic_centrality(vertices=None, mode=ALL, cutoff=None, weights=None, " + "normalized=True)\n--\n\n" "Calculates the harmonic centralities of given vertices in a graph.\n\n" "The harmonic centerality of a vertex measures how easily other\n" "vertices can be reached from it (or the other way: how easily it\n" @@ -13024,7 +13024,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_clusters */ {"clusters", (PyCFunction) igraphmodule_Graph_clusters, METH_VARARGS | METH_KEYWORDS, - "clusters(mode=STRONG)\n\n" + "clusters(mode=STRONG)\n--\n\n" "Calculates the (strong or weak) clusters for a given graph.\n\n" "@attention: this function has a more convenient interface in class\n" " L{Graph} which wraps the result in a L{VertexClustering} object.\n" @@ -13034,7 +13034,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the component index for every node in the graph.\n"}, {"copy", (PyCFunction) igraphmodule_Graph_copy, METH_NOARGS, - "copy()\n\n" + "copy()\n--\n\n" "Creates a copy of the graph.\n\n" "Attributes are copied by reference; in other words, if you use\n" "mutable Python objects as attribute values, these objects will still\n" @@ -13043,7 +13043,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"decompose", (PyCFunction) igraphmodule_Graph_decompose, METH_VARARGS | METH_KEYWORDS, - "decompose(mode=STRONG, maxcompno=None, minelements=1)\n\n" + "decompose(mode=STRONG, maxcompno=None, minelements=1)\n--\n\n" "Decomposes the graph into subgraphs.\n\n" "@param mode: must be either STRONG or WEAK, depending on the\n" " clusters being sought.\n" @@ -13057,7 +13057,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_contract_vertices */ {"contract_vertices", (PyCFunction) igraphmodule_Graph_contract_vertices, METH_VARARGS | METH_KEYWORDS, - "contract_vertices(mapping, combine_attrs=None)\n\n" + "contract_vertices(mapping, combine_attrs=None)\n--\n\n" "Contracts some vertices in the graph, i.e. replaces groups of vertices\n" "with single vertices. Edges are not affected.\n\n" "@param mapping: numeric vector which gives the mapping between old and\n" @@ -13082,7 +13082,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_constraint */ {"constraint", (PyCFunction) igraphmodule_Graph_constraint, METH_VARARGS | METH_KEYWORDS, - "constraint(vertices=None, weights=None)\n\n" + "constraint(vertices=None, weights=None)\n--\n\n" "Calculates Burt's constraint scores for given vertices in a graph.\n\n" "Burt's constraint is higher if ego has less, or mutually stronger\n" "related (i.e. more redundant) contacts. Burt's measure of\n" @@ -13102,7 +13102,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_density */ {"density", (PyCFunction) igraphmodule_Graph_density, METH_VARARGS | METH_KEYWORDS, - "density(loops=False)\n\n" + "density(loops=False) -> float\n--\n\n" "Calculates the density of the graph.\n\n" "@param loops: whether to take loops into consideration. If C{True},\n" " the algorithm assumes that there might be some loops in the graph\n" @@ -13113,7 +13113,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interfaces to igraph_diameter */ {"diameter", (PyCFunction) igraphmodule_Graph_diameter, METH_VARARGS | METH_KEYWORDS, - "diameter(directed=True, unconn=True, weights=None)\n\n" + "diameter(directed=True, unconn=True, weights=None)\n--\n\n" "Calculates the diameter of the graph.\n\n" "@param directed: whether to consider directed paths.\n" "@param unconn: if C{True} and the graph is unconnected, the\n" @@ -13126,7 +13126,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the diameter"}, {"get_diameter", (PyCFunction) igraphmodule_Graph_get_diameter, METH_VARARGS | METH_KEYWORDS, - "get_diameter(directed=True, unconn=True, weights=None)\n\n" + "get_diameter(directed=True, unconn=True, weights=None)\n--\n\n" "Returns a path with the actual diameter of the graph.\n\n" "If there are many shortest paths with the length of the diameter,\n" "it returns the first one it founds.\n\n" @@ -13141,7 +13141,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the vertices in the path in order."}, {"farthest_points", (PyCFunction) igraphmodule_Graph_farthest_points, METH_VARARGS | METH_KEYWORDS, - "farthest_points(directed=True, unconn=True, weights=None)\n\n" + "farthest_points(directed=True, unconn=True, weights=None)\n--\n\n" "Returns two vertex IDs whose distance equals the actual diameter\n" "of the graph.\n\n" "If there are many shortest paths with the length of the diameter,\n" @@ -13161,7 +13161,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_diversity */ {"diversity", (PyCFunction) igraphmodule_Graph_diversity, METH_VARARGS | METH_KEYWORDS, - "diversity(vertices=None, weights=None)\n\n" + "diversity(vertices=None, weights=None)\n--\n\n" "Calculates the structural diversity index of the vertices.\n\n" "The structural diversity index of a vertex is simply the (normalized)\n" "Shannon entropy of the weights of the edges incident on the vertex.\n\n" @@ -13181,7 +13181,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_eccentricity */ {"eccentricity", (PyCFunction) igraphmodule_Graph_eccentricity, METH_VARARGS | METH_KEYWORDS, - "eccentricity(vertices=None, mode=ALL)\n\n" + "eccentricity(vertices=None, mode=ALL)\n--\n\n" "Calculates the eccentricities of given vertices in a graph.\n\n" "The eccentricity of a vertex is calculated by measuring the\n" "shortest distance from (or to) the vertex, to (or from) all other\n" @@ -13198,7 +13198,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_edge_betweenness[_estimate] */ {"edge_betweenness", (PyCFunction) igraphmodule_Graph_edge_betweenness, METH_VARARGS | METH_KEYWORDS, - "edge_betweenness(directed=True, cutoff=None, weights=None)\n\n" + "edge_betweenness(directed=True, cutoff=None, weights=None)\n--\n\n" "Calculates or estimates the edge betweennesses in a graph.\n\n" "@param directed: whether to consider directed paths.\n" "@param cutoff: if it is an integer, only paths less than or equal to this\n" @@ -13217,7 +13217,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_[st_]edge_connectivity */ {"edge_connectivity", (PyCFunction) igraphmodule_Graph_edge_connectivity, METH_VARARGS | METH_KEYWORDS, - "edge_connectivity(source=-1, target=-1, checks=True)\n\n" + "edge_connectivity(source=-1, target=-1, checks=True)\n--\n\n" "Calculates the edge connectivity of the graph or between some vertices.\n\n" "The edge connectivity between two given vertices is the number of edges\n" "that have to be removed in order to disconnect the two vertices into two\n" @@ -13243,7 +13243,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"eigenvector_centrality", (PyCFunction) igraphmodule_Graph_eigenvector_centrality, METH_VARARGS | METH_KEYWORDS, - "eigenvector_centrality(directed=True, scale=True, weights=None, return_eigenvalue=False, arpack_options=None)\n\n" + "eigenvector_centrality(directed=True, scale=True, weights=None, " + "return_eigenvalue=False, arpack_options=None)\n--\n\n" "Calculates the eigenvector centralities of the vertices in a graph.\n\n" "Eigenvector centrality is a measure of the importance of a node in a\n" "network. It assigns relative scores to all nodes in the network based\n" @@ -13278,7 +13279,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_feedback_arc_set */ {"feedback_arc_set", (PyCFunction) igraphmodule_Graph_feedback_arc_set, METH_VARARGS | METH_KEYWORDS, - "feedback_arc_set(weights=None, method=\"eades\")\n\n" + "feedback_arc_set(weights=None, method=\"eades\")\n--\n\n" "Calculates an approximately or exactly minimal feedback arc set.\n\n" "A feedback arc set is a set of edges whose removal makes the graph acyclic.\n" "Since this is always possible by removing all the edges, we are in general\n" @@ -13306,7 +13307,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_get_shortest_paths {"get_shortest_paths", (PyCFunction) igraphmodule_Graph_get_shortest_paths, METH_VARARGS | METH_KEYWORDS, - "get_shortest_paths(v, to=None, weights=None, mode=OUT, output=\"vpath\")\n\n" + "get_shortest_paths(v, to=None, weights=None, mode=OUT, output=\"vpath\")\n--\n\n" "Calculates the shortest paths from/to a given node in a graph.\n\n" "@param v: the source/destination for the calculated paths\n" "@param to: a vertex selector describing the destination/source for\n" @@ -13331,7 +13332,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"get_all_shortest_paths", (PyCFunction) igraphmodule_Graph_get_all_shortest_paths, METH_VARARGS | METH_KEYWORDS, - "get_all_shortest_paths(v, to=None, weights=None, mode=OUT)\n\n" + "get_all_shortest_paths(v, to=None, weights=None, mode=OUT)\n--\n\n" "Calculates all of the shortest paths from/to a given node in a graph.\n\n" "@param v: the source for the calculated paths\n" "@param to: a vertex selector describing the destination for\n" @@ -13352,7 +13353,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"_get_all_simple_paths", (PyCFunction) igraphmodule_Graph_get_all_simple_paths, METH_VARARGS | METH_KEYWORDS, - "_get_all_simple_paths(v, to=None, cutoff=-1, mode=OUT)\n\n" + "_get_all_simple_paths(v, to=None, cutoff=-1, mode=OUT)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.get_all_simple_paths()\n\n" }, @@ -13360,7 +13361,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_girth */ {"girth", (PyCFunction)igraphmodule_Graph_girth, METH_VARARGS | METH_KEYWORDS, - "girth(return_shortest_circle=False)\n\n" + "girth(return_shortest_circle=False)\n--\n\n" "Returns the girth of the graph.\n\n" "The girth of a graph is the length of the shortest circle in it.\n\n" "@param return_shortest_circle: whether to return one of the shortest\n" @@ -13372,21 +13373,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_convergence_degree */ {"convergence_degree", (PyCFunction)igraphmodule_Graph_convergence_degree, METH_NOARGS, - "convergence_degree()\n\n" + "convergence_degree()\n--\n\n" "Undocumented (yet)." }, /* interface to igraph_convergence_field_size */ {"convergence_field_size", (PyCFunction)igraphmodule_Graph_convergence_field_size, METH_NOARGS, - "convergence_field_size()\n\n" + "convergence_field_size()\n--\n\n" "Undocumented (yet)." }, /* interface to igraph_hub_score */ {"hub_score", (PyCFunction)igraphmodule_Graph_hub_score, METH_VARARGS | METH_KEYWORDS, - "hub_score(weights=None, scale=True, arpack_options=None, return_eigenvalue=False)\n\n" + "hub_score(weights=None, scale=True, arpack_options=None, return_eigenvalue=False)\n--\n\n" "Calculates Kleinberg's hub score for the vertices of the graph\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" " even an edge attribute name.\n" @@ -13404,7 +13405,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_induced_subgraph */ {"induced_subgraph", (PyCFunction) igraphmodule_Graph_induced_subgraph, METH_VARARGS | METH_KEYWORDS, - "induced_subgraph(vertices, implementation=\"auto\")\n\n" + "induced_subgraph(vertices, implementation=\"auto\")\n--\n\n" "Returns a subgraph spanned by the given vertices.\n\n" "@param vertices: a list containing the vertex IDs which\n" " should be included in the result.\n" @@ -13425,7 +13426,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_bipartite */ {"is_bipartite", (PyCFunction) igraphmodule_Graph_is_bipartite, METH_VARARGS | METH_KEYWORDS, - "is_bipartite(return_types=False)\n\n" + "is_bipartite(return_types=False) -> bool\n--\n\n" "Decides whether the graph is bipartite or not.\n\n" "Vertices of a bipartite graph can be partitioned into two groups A\n" "and B in a way that all edges go between the two groups.\n\n" @@ -13444,7 +13445,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_avg_nearest_neighbor_degree */ {"knn", (PyCFunction) igraphmodule_Graph_knn, METH_VARARGS | METH_KEYWORDS, - "knn(vids=None, weights=None)\n\n" + "knn(vids=None, weights=None)\n--\n\n" "Calculates the average degree of the neighbors for each vertex, and\n" "the same quantity as the function of vertex degree.\n\n" "@param vids: the vertices for which the calculation is performed.\n" @@ -13463,7 +13464,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_connected */ {"is_connected", (PyCFunction) igraphmodule_Graph_is_connected, METH_VARARGS | METH_KEYWORDS, - "is_connected(mode=STRONG)\n\n" + "is_connected(mode=STRONG) -> bool\n--\n\n" "Decides whether the graph is connected.\n\n" "@param mode: whether we should calculate strong or weak connectivity.\n" "@return: C{True} if the graph is connected, C{False} otherwise.\n"}, @@ -13471,7 +13472,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_linegraph */ {"linegraph", (PyCFunction) igraphmodule_Graph_linegraph, METH_VARARGS | METH_KEYWORDS, - "linegraph()\n\n" + "linegraph()\n--\n\n" "Returns the line graph of the graph.\n\n" "The line graph M{L(G)} of an undirected graph is defined as follows:\n" "M{L(G)} has one vertex for each edge in G and two vertices in M{L(G)}\n" @@ -13486,7 +13487,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_maxdegree */ {"maxdegree", (PyCFunction) igraphmodule_Graph_maxdegree, METH_VARARGS | METH_KEYWORDS, - "maxdegree(vertices=None, mode=ALL, loops=False)\n\n" + "maxdegree(vertices=None, mode=ALL, loops=False)\n--\n\n" "Returns the maximum degree of a vertex set in the graph.\n\n" "This method accepts a single vertex ID or a list of vertex IDs as a\n" "parameter, and returns the degree of the given vertices (in the\n" @@ -13503,7 +13504,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighborhood */ {"neighborhood", (PyCFunction) igraphmodule_Graph_neighborhood, METH_VARARGS | METH_KEYWORDS, - "neighborhood(vertices=None, order=1, mode=ALL, mindist=0)\n\n" + "neighborhood(vertices=None, order=1, mode=ALL, mindist=0)\n--\n\n" "For each vertex specified by I{vertices}, returns the\n" "vertices reachable from that vertex in at most I{order} steps. If\n" "I{mindist} is larger than zero, vertices that are reachable in less\n" @@ -13531,7 +13532,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighborhood_size */ {"neighborhood_size", (PyCFunction) igraphmodule_Graph_neighborhood_size, METH_VARARGS | METH_KEYWORDS, - "neighborhood_size(vertices=None, order=1, mode=ALL, mindist=0)\n\n" + "neighborhood_size(vertices=None, order=1, mode=ALL, mindist=0)\n--\n\n" "For each vertex specified by I{vertices}, returns the number of\n" "vertices reachable from that vertex in at most I{order} steps. If\n" "I{mindist} is larger than zero, vertices that are reachable in less\n" @@ -13562,7 +13563,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "personalized_pagerank(vertices=None, directed=True, damping=0.85,\n" " reset=None, reset_vertices=None, weights=None, \n" " arpack_options=None, implementation=\"prpack\", niter=1000,\n" - " eps=0.001)\n\n" + " eps=0.001)\n--\n\n" "Calculates the personalized PageRank values of a graph.\n\n" "The personalized PageRank calculation is similar to the PageRank\n" "calculation, but the random walk is reset to a non-uniform distribution\n" @@ -13611,7 +13612,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_path_length_hist */ {"path_length_hist", (PyCFunction) igraphmodule_Graph_path_length_hist, METH_VARARGS | METH_KEYWORDS, - "path_length_hist(directed=True)\n\n" + "path_length_hist(directed=True)\n--\n\n" "Calculates the path length histogram of the graph\n" "@attention: this function is wrapped in a more convenient syntax in the\n" " derived class L{Graph}. It is advised to use that instead of this version.\n\n" @@ -13625,7 +13626,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_permute_vertices */ {"permute_vertices", (PyCFunction) igraphmodule_Graph_permute_vertices, METH_VARARGS | METH_KEYWORDS, - "permute_vertices(permutation)\n\n" + "permute_vertices(permutation)\n--\n\n" "Permutes the vertices of the graph according to the given permutation\n" "and returns the new graph.\n\n" "Vertex M{k} of the original graph will become vertex M{permutation[k]}\n" @@ -13637,7 +13638,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interfaces to igraph_radius */ {"radius", (PyCFunction) igraphmodule_Graph_radius, METH_VARARGS | METH_KEYWORDS, - "radius(mode=OUT)\n\n" + "radius(mode=OUT)\n--\n\n" "Calculates the radius of the graph.\n\n" "The radius of a graph is defined as the minimum eccentricity of\n" "its vertices (see L{eccentricity()}).\n" @@ -13653,7 +13654,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_reciprocity */ {"reciprocity", (PyCFunction) igraphmodule_Graph_reciprocity, METH_VARARGS | METH_KEYWORDS, - "reciprocity(ignore_loops=True, mode=\"default\")\n\n" + "reciprocity(ignore_loops=True, mode=\"default\") -> float\n--\n\n" "Reciprocity defines the proportion of mutual connections in a\n" "directed graph. It is most commonly defined as the probability\n" "that the opposite counterpart of a directed edge is also included\n" @@ -13677,7 +13678,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_rewire */ {"rewire", (PyCFunction) igraphmodule_Graph_rewire, METH_VARARGS | METH_KEYWORDS, - "rewire(n=1000, mode=\"simple\")\n\n" + "rewire(n=1000, mode=\"simple\")\n--\n\n" "Randomly rewires the graph while preserving the degree distribution.\n\n" "Please note that the rewiring is done \"in-place\", so the original\n" "graph will be modified. If you want to preserve the original graph,\n" @@ -13690,7 +13691,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_rewire_edges */ {"rewire_edges", (PyCFunction) igraphmodule_Graph_rewire_edges, METH_VARARGS | METH_KEYWORDS, - "rewire_edges(prob, loops=False, multiple=False)\n\n" + "rewire_edges(prob, loops=False, multiple=False)\n--\n\n" "Rewires the edges of a graph with constant probability.\n\n" "Each endpoint of each edge of the graph will be rewired with a constant\n" "probability, given in the first argument.\n\n" @@ -13705,7 +13706,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_shortest_paths */ {"shortest_paths", (PyCFunction) igraphmodule_Graph_shortest_paths, METH_VARARGS | METH_KEYWORDS, - "shortest_paths(source=None, target=None, weights=None, mode=OUT)\n\n" + "shortest_paths(source=None, target=None, weights=None, mode=OUT)\n--\n\n" "Calculates shortest path lengths for given vertices in a graph.\n\n" "The algorithm used for the calculations is selected automatically:\n" "a simple BFS is used for unweighted graphs, Dijkstra's algorithm is\n" @@ -13728,7 +13729,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_simplify */ {"simplify", (PyCFunction) igraphmodule_Graph_simplify, METH_VARARGS | METH_KEYWORDS, - "simplify(multiple=True, loops=True, combine_edges=None)\n\n" + "simplify(multiple=True, loops=True, combine_edges=None)\n--\n\n" "Simplifies a graph by removing self-loops and/or multiple edges.\n\n" "\n" "For example, suppose you have a graph with an edge attribute named\n" @@ -13780,14 +13781,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_minimum_spanning_tree */ {"_spanning_tree", (PyCFunction) igraphmodule_Graph_spanning_tree, METH_VARARGS | METH_KEYWORDS, - "_spanning_tree(weights=None)\n\n" + "_spanning_tree(weights=None)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.spanning_tree()"}, // interface to igraph_subcomponent {"subcomponent", (PyCFunction) igraphmodule_Graph_subcomponent, METH_VARARGS | METH_KEYWORDS, - "subcomponent(v, mode=ALL)\n\n" + "subcomponent(v, mode=ALL)\n--\n\n" "Determines the indices of vertices which are in the same component as a given vertex.\n\n" "@param v: the index of the vertex used as the source/destination\n" "@param mode: if equals to L{IN}, returns the vertex IDs from\n" @@ -13802,7 +13803,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_subgraph_edges */ {"subgraph_edges", (PyCFunction) igraphmodule_Graph_subgraph_edges, METH_VARARGS | METH_KEYWORDS, - "subgraph_edges(edges, delete_vertices=True)\n\n" + "subgraph_edges(edges, delete_vertices=True)\n--\n\n" "Returns a subgraph spanned by the given edges.\n\n" "@param edges: a list containing the edge IDs which should\n" " be included in the result.\n" @@ -13815,7 +13816,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"topological_sorting", (PyCFunction) igraphmodule_Graph_topological_sorting, METH_VARARGS | METH_KEYWORDS, - "topological_sorting(mode=OUT)\n\n" + "topological_sorting(mode=OUT)\n--\n\n" "Calculates a possible topological sorting of the graph.\n\n" "Returns a partial sorting and issues a warning if the graph is not\n" "a directed acyclic graph.\n\n" @@ -13828,7 +13829,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"to_prufer", (PyCFunction) igraphmodule_Graph_to_prufer, METH_NOARGS, - "to_prufer()\n\n" + "to_prufer()\n--\n\n" "Converts a tree graph into a Prufer sequence.\n\n" "@return: the Prufer sequence as a list" }, @@ -13837,7 +13838,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"transitivity_undirected", (PyCFunction) igraphmodule_Graph_transitivity_undirected, METH_VARARGS | METH_KEYWORDS, - "transitivity_undirected(mode=\"nan\")\n\n" + "transitivity_undirected(mode=\"nan\")\n--\n\n" "Calculates the global transitivity (clustering coefficient) of the\n" "graph.\n\n" "The transitivity measures the probability that two neighbors of a\n" @@ -13863,7 +13864,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"transitivity_local_undirected", (PyCFunction) igraphmodule_Graph_transitivity_local_undirected, METH_VARARGS | METH_KEYWORDS, - "transitivity_local_undirected(vertices=None, mode=\"nan\", weights=None)\n\n" + "transitivity_local_undirected(vertices=None, mode=\"nan\", weights=None)\n--\n\n" "Calculates the local transitivity (clustering coefficient) of the\n" "given vertices in the graph.\n\n" "The transitivity measures the probability that two neighbors of a\n" @@ -13897,7 +13898,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"transitivity_avglocal_undirected", (PyCFunction) igraphmodule_Graph_transitivity_avglocal_undirected, METH_VARARGS | METH_KEYWORDS, - "transitivity_avglocal_undirected(mode=\"nan\")\n\n" + "transitivity_avglocal_undirected(mode=\"nan\")\n--\n\n" "Calculates the average of the vertex transitivities of the graph.\n\n" "The transitivity measures the probability that two neighbors of a\n" "vertex are connected. In case of the average local transitivity,\n" @@ -13922,7 +13923,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_unfold_tree */ {"unfold_tree", (PyCFunction) igraphmodule_Graph_unfold_tree, METH_VARARGS | METH_KEYWORDS, - "unfold_tree(sources=None, mode=OUT)\n\n" + "unfold_tree(sources=None, mode=OUT)\n--\n\n" "Unfolds the graph using a BFS to a tree by duplicating vertices as necessary.\n\n" "@param sources: the source vertices to start the unfolding from. It should be a\n" " list of vertex indices, preferably one vertex from each connected component.\n" @@ -13938,7 +13939,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_[st_]vertex_connectivity */ {"vertex_connectivity", (PyCFunction) igraphmodule_Graph_vertex_connectivity, METH_VARARGS | METH_KEYWORDS, - "vertex_connectivity(source=-1, target=-1, checks=True, neighbors=\"error\")\n\n" + "vertex_connectivity(source=-1, target=-1, checks=True, neighbors=\"error\")\n--\n\n" "Calculates the vertex connectivity of the graph or between some vertices.\n\n" "The vertex connectivity between two given vertices is the number of vertices\n" "that have to be removed in order to disconnect the two vertices into two\n" @@ -13971,7 +13972,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_bibcoupling */ {"bibcoupling", (PyCFunction) igraphmodule_Graph_bibcoupling, METH_VARARGS | METH_KEYWORDS, - "bibcoupling(vertices=None)\n\n" + "bibcoupling(vertices=None)\n--\n\n" "Calculates bibliographic coupling scores for given vertices in a graph.\n\n" "@param vertices: the vertices to be analysed. If C{None}, all vertices\n" " will be considered.\n" @@ -13979,7 +13980,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_cocitation */ {"cocitation", (PyCFunction) igraphmodule_Graph_cocitation, METH_VARARGS | METH_KEYWORDS, - "cocitation(vertices=None)\n\n" + "cocitation(vertices=None)\n--\n\n" "Calculates cocitation scores for given vertices in a graph.\n\n" "@param vertices: the vertices to be analysed. If C{None}, all vertices\n" " will be considered.\n" @@ -14014,7 +14015,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"similarity_inverse_log_weighted", (PyCFunction) igraphmodule_Graph_similarity_inverse_log_weighted, METH_VARARGS | METH_KEYWORDS, - "similarity_inverse_log_weighted(vertices=None, mode=IGRAPH_ALL)\n\n" + "similarity_inverse_log_weighted(vertices=None, mode=IGRAPH_ALL)\n--\n\n" "Inverse log-weighted similarity coefficient of vertices.\n\n" "Each vertex is assigned a weight which is 1 / log(degree). The\n" "log-weighted similarity of two vertices is the sum of the weights\n" @@ -14031,7 +14032,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_similarity_jaccard */ {"similarity_jaccard", (PyCFunction) igraphmodule_Graph_similarity_jaccard, METH_VARARGS | METH_KEYWORDS, - "similarity_jaccard(vertices=None, pairs=None, mode=IGRAPH_ALL, loops=True)\n\n" + "similarity_jaccard(vertices=None, pairs=None, mode=IGRAPH_ALL, loops=True)\n--\n\n" "Jaccard similarity coefficient of vertices.\n\n" "The Jaccard similarity coefficient of two vertices is the number of their\n" "common neighbors divided by the number of vertices that are adjacent to\n" @@ -14059,7 +14060,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /******************/ {"motifs_randesu", (PyCFunction) igraphmodule_Graph_motifs_randesu, METH_VARARGS | METH_KEYWORDS, - "motifs_randesu(size=3, cut_prob=None, callback=None)\n\n" + "motifs_randesu(size=3, cut_prob=None, callback=None)\n--\n\n" "Counts the number of motifs in the graph\n\n" "Motifs are small subgraphs of a given structure in a graph. It is\n" "argued that the motif profile (ie. the number of different motifs in\n" @@ -14090,7 +14091,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"motifs_randesu_no", (PyCFunction) igraphmodule_Graph_motifs_randesu_no, METH_VARARGS | METH_KEYWORDS, - "motifs_randesu_no(size=3, cut_prob=None)\n\n" + "motifs_randesu_no(size=3, cut_prob=None)\n--\n\n" "Counts the total number of motifs in the graph\n\n" "Motifs are small subgraphs of a given structure in a graph.\n" "This function counts the total number of motifs in a graph without\n" @@ -14107,7 +14108,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"motifs_randesu_estimate", (PyCFunction) igraphmodule_Graph_motifs_randesu_estimate, METH_VARARGS | METH_KEYWORDS, - "motifs_randesu_estimate(size=3, cut_prob=None, sample)\n\n" + "motifs_randesu_estimate(size=3, cut_prob=None, sample)\n--\n\n" "Counts the total number of motifs in the graph\n\n" "Motifs are small subgraphs of a given structure in a graph.\n" "This function estimates the total number of motifs in a graph without\n" @@ -14126,7 +14127,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"dyad_census", (PyCFunction) igraphmodule_Graph_dyad_census, METH_NOARGS, - "dyad_census()\n\n" + "dyad_census()\n--\n\n" "Dyad census, as defined by Holland and Leinhardt\n\n" "Dyad census means classifying each pair of vertices of a directed\n" "graph into three categories: mutual, there is an edge from I{a} to\n" @@ -14141,7 +14142,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"triad_census", (PyCFunction) igraphmodule_Graph_triad_census, METH_NOARGS, - "triad_census()\n\n" + "triad_census()\n--\n\n" "Triad census, as defined by Davis and Leinhardt\n\n" "Calculating the triad census means classifying every triplets of\n" "vertices in a directed graph. A triplet can be in one of 16 states,\n" @@ -14161,7 +14162,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_bipartite", (PyCFunction) igraphmodule_Graph_layout_bipartite, METH_VARARGS | METH_KEYWORDS, - "layout_bipartite(types=\"type\", hgap=1, vgap=1, maxiter=100)\n\n" + "layout_bipartite(types=\"type\", hgap=1, vgap=1, maxiter=100)\n--\n\n" "Place the vertices of a bipartite graph in two layers.\n\n" "The layout is created by placing the vertices in two rows, according\n" "to their types. The positions of the vertices within the rows are\n" @@ -14180,7 +14181,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_layout_circle */ {"layout_circle", (PyCFunction) igraphmodule_Graph_layout_circle, METH_VARARGS | METH_KEYWORDS, - "layout_circle(dim=2, order=None)\n\n" + "layout_circle(dim=2, order=None)\n--\n\n" "Places the vertices of the graph uniformly on a circle or a sphere.\n\n" "@param dim: the desired number of dimensions for the layout. dim=2\n" " means a 2D layout, dim=3 means a 3D layout.\n" @@ -14191,7 +14192,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_layout_grid */ {"layout_grid", (PyCFunction) igraphmodule_Graph_layout_grid, METH_VARARGS | METH_KEYWORDS, - "layout_grid(width=0, height=0, dim=2)\n\n" + "layout_grid(width=0, height=0, dim=2)\n--\n\n" "Places the vertices of a graph in a 2D or 3D grid.\n\n" "@param width: the number of vertices in a single row of the layout.\n" " Zero or negative numbers mean that the width should be determined\n" @@ -14206,7 +14207,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_layout_star */ {"layout_star", (PyCFunction) igraphmodule_Graph_layout_star, METH_VARARGS | METH_KEYWORDS, - "layout_star(center=0, order=None)\n\n" + "layout_star(center=0, order=None)\n--\n\n" "Calculates a star-like layout for the graph.\n\n" "@param center: the ID of the vertex to put in the center\n" "@param order: a numeric vector giving the order of the vertices\n" @@ -14219,9 +14220,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_kamada_kawai", (PyCFunction) igraphmodule_Graph_layout_kamada_kawai, METH_VARARGS | METH_KEYWORDS, - "layout_kamada_kawai(maxiter=1000, seed=None, maxiter=1000, epsilon=0, \n" - " kkconst=None, minx=None, maxx=None, miny=None, maxy=None, \n" - " minz=None, maxz=None, dim=2)\n\n" + "layout_kamada_kawai(maxiter=1000, seed=None, maxiter=1000, epsilon=0, " + "kkconst=None, minx=None, maxx=None, miny=None, maxy=None, " + "minz=None, maxz=None, dim=2)\n--\n\n" "Places the vertices on a plane according to the Kamada-Kawai algorithm.\n\n" "This is a force directed layout, see Kamada, T. and Kawai, S.:\n" "An Algorithm for Drawing General Undirected Graphs.\n" @@ -14253,9 +14254,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_davidson_harel", (PyCFunction) igraphmodule_Graph_layout_davidson_harel, METH_VARARGS | METH_KEYWORDS, - "layout_davidson_harel(seed=None, maxiter=10, fineiter=-1, cool_fact=0.75,\n" - " weight_node_dist=1.0, weight_border=0.0, weight_edge_lengths=-1,\n" - " weight_edge_crossings=-1, weight_node_edge_dist=-1)\n\n" + "layout_davidson_harel(seed=None, maxiter=10, fineiter=-1, cool_fact=0.75, " + "weight_node_dist=1.0, weight_border=0.0, weight_edge_lengths=-1, " + "weight_edge_crossings=-1, weight_node_edge_dist=-1)\n--\n\n" "Places the vertices on a 2D plane according to the Davidson-Harel layout\n" "algorithm.\n\n" "The algorithm uses simulated annealing and a sophisticated energy function,\n" @@ -14293,7 +14294,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_drl", (PyCFunction) igraphmodule_Graph_layout_drl, METH_VARARGS | METH_KEYWORDS, - "layout_drl(weights=None, fixed=None, seed=None, options=None, dim=2)\n\n" + "layout_drl(weights=None, fixed=None, seed=None, options=None, dim=2)\n--\n\n" "Places the vertices on a 2D plane or in the 3D space ccording to the DrL\n" "layout algorithm.\n\n" "This is an algorithm suitable for quite large graphs, but it can be\n" @@ -14352,9 +14353,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_fruchterman_reingold", (PyCFunction) igraphmodule_Graph_layout_fruchterman_reingold, METH_VARARGS | METH_KEYWORDS, - "layout_fruchterman_reingold(weights=None, niter=500, seed=None, \n" - " start_temp=None, minx=None, maxx=None, miny=None, \n" - " maxy=None, minz=None, maxz=None, grid=\"auto\")\n\n" + "layout_fruchterman_reingold(weights=None, niter=500, seed=None, " + "start_temp=None, minx=None, maxx=None, miny=None, " + "maxy=None, minz=None, maxz=None, grid=\"auto\")\n--\n\n" "Places the vertices on a 2D plane according to the\n" "Fruchterman-Reingold algorithm.\n\n" "This is a force directed layout, see Fruchterman, T. M. J. and Reingold, E. M.:\n" @@ -14394,7 +14395,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_graphopt", (PyCFunction) igraphmodule_Graph_layout_graphopt, METH_VARARGS | METH_KEYWORDS, - "layout_graphopt(niter=500, node_charge=0.001, node_mass=30, spring_length=0, spring_constant=1, max_sa_movement=5, seed=None)\n\n" + "layout_graphopt(niter=500, node_charge=0.001, node_mass=30, " + "spring_length=0, spring_constant=1, max_sa_movement=5, seed=None)\n--\n\n" "This is a port of the graphopt layout algorithm by Michael Schmuhl.\n" "graphopt version 0.4.1 was rewritten in C and the support for layers\n" "was removed.\n\n" @@ -14420,7 +14422,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_layout_lgl */ {"layout_lgl", (PyCFunction) igraphmodule_Graph_layout_lgl, METH_VARARGS | METH_KEYWORDS, - "layout_lgl(maxiter=150, maxdelta=-1, area=-1, coolexp=1.5, repulserad=-1, cellsize=-1, root=None)\n\n" + "layout_lgl(maxiter=150, maxdelta=-1, area=-1, coolexp=1.5, " + "repulserad=-1, cellsize=-1, root=None)\n--\n\n" "Places the vertices on a 2D plane according to the Large Graph Layout.\n\n" "@param maxiter: the number of iterations to perform.\n" "@param maxdelta: the maximum distance to move a vertex in\n" @@ -14446,7 +14449,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_mds", (PyCFunction) igraphmodule_Graph_layout_mds, METH_VARARGS | METH_KEYWORDS, - "layout_mds(dist=None, dim=2, arpack_options=None)\n" + "layout_mds(dist=None, dim=2, arpack_options=None)\n--\n" "Places the vertices in an Euclidean space with the given number of\n" "dimensions using multidimensional scaling.\n\n" "This layout requires a distance matrix, where the intersection of\n" @@ -14479,7 +14482,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_reingold_tilford", (PyCFunction) igraphmodule_Graph_layout_reingold_tilford, METH_VARARGS | METH_KEYWORDS, - "layout_reingold_tilford(mode=\"out\", root=None, rootlevel=None)\n" + "layout_reingold_tilford(mode=\"out\", root=None, rootlevel=None)\n--\n" "Places the vertices on a 2D plane according to the Reingold-Tilford\n" "layout algorithm.\n\n" "This is a tree layout. If the given graph is not a tree, a breadth-first\n" @@ -14509,7 +14512,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_reingold_tilford_circular", (PyCFunction) igraphmodule_Graph_layout_reingold_tilford_circular, METH_VARARGS | METH_KEYWORDS, - "layout_reingold_tilford_circular(mode=\"out\", root=None, rootlevel=None)\n" + "layout_reingold_tilford_circular(mode=\"out\", root=None, rootlevel=None)\n--\n" "Circular Reingold-Tilford layout for trees.\n\n" "This layout is similar to the Reingold-Tilford layout, but the vertices\n" "are placed in a circular way, with the root vertex in the center.\n\n" @@ -14523,7 +14526,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_layout_random */ {"layout_random", (PyCFunction) igraphmodule_Graph_layout_random, METH_VARARGS | METH_KEYWORDS, - "layout_random(dim=2)\n" + "layout_random(dim=2)\n--\n" "Places the vertices of the graph randomly.\n\n" "@param dim: the desired number of dimensions for the layout. dim=2\n" " means a 2D layout, dim=3 means a 3D layout.\n" @@ -14541,7 +14544,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { //////////////////////////// {"bfs", (PyCFunction) igraphmodule_Graph_bfs, METH_VARARGS | METH_KEYWORDS, - "bfs(vid, mode=OUT)\n\n" + "bfs(vid, mode=OUT)\n--\n\n" "Conducts a breadth first search (BFS) on the graph.\n\n" "@param vid: the root vertex ID\n" "@param mode: either L{IN} or L{OUT} or L{ALL}, ignored\n" @@ -14552,7 +14555,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " - The parent of every vertex in the BFS\n"}, {"bfsiter", (PyCFunction) igraphmodule_Graph_bfsiter, METH_VARARGS | METH_KEYWORDS, - "bfsiter(vid, mode=OUT, advanced=False)\n\n" + "bfsiter(vid, mode=OUT, advanced=False)\n--\n\n" "Constructs a breadth first search (BFS) iterator of the graph.\n\n" "@param vid: the root vertex ID\n" "@param mode: either L{IN} or L{OUT} or L{ALL}.\n" @@ -14563,7 +14566,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the BFS iterator as an L{igraph.BFSIter} object.\n"}, {"dfsiter", (PyCFunction) igraphmodule_Graph_dfsiter, METH_VARARGS | METH_KEYWORDS, - "dfsiter(vid, mode=OUT, advanced=False)\n\n" + "dfsiter(vid, mode=OUT, advanced=False)\n--\n\n" "Constructs a depth first search (DFS) iterator of the graph.\n\n" "@param vid: the root vertex ID\n" "@param mode: either L{IN} or L{OUT} or L{ALL}.\n" @@ -14580,7 +14583,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_get_adjacency {"get_adjacency", (PyCFunction) igraphmodule_Graph_get_adjacency, METH_VARARGS | METH_KEYWORDS, - "get_adjacency(type=GET_ADJACENCY_BOTH, eids=False)\n\n" + "get_adjacency(type=GET_ADJACENCY_BOTH, eids=False)\n--\n\n" "Returns the adjacency matrix of a graph.\n\n" "@param type: either C{GET_ADJACENCY_LOWER} (uses the\n" " lower triangle of the matrix) or C{GET_ADJACENCY_UPPER}\n" @@ -14596,19 +14599,19 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_get_edgelist {"get_edgelist", (PyCFunction) igraphmodule_Graph_get_edgelist, METH_NOARGS, - "get_edgelist()\n\n" "Returns the edge list of a graph."}, + "get_edgelist()\n--\n\n" "Returns the edge list of a graph."}, /* interface to igraph_get_incidence */ {"get_incidence", (PyCFunction) igraphmodule_Graph_get_incidence, METH_VARARGS | METH_KEYWORDS, - "get_incidence(types)\n\n" + "get_incidence(types)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.get_incidence()\n\n"}, // interface to igraph_to_directed {"to_directed", (PyCFunction) igraphmodule_Graph_to_directed, METH_VARARGS | METH_KEYWORDS, - "to_directed(mutual=True)\n\n" + "to_directed(mutual=True)\n--\n\n" "Converts an undirected graph to directed.\n\n" "@param mutual: C{True} if mutual directed edges should be\n" " created for every undirected edge. If C{False}, a directed\n" @@ -14617,7 +14620,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_to_undirected {"to_undirected", (PyCFunction) igraphmodule_Graph_to_undirected, METH_VARARGS | METH_KEYWORDS, - "to_undirected(mode=\"collapse\", combine_edges=None)\n\n" + "to_undirected(mode=\"collapse\", combine_edges=None)\n--\n\n" "Converts a directed graph to undirected.\n\n" "@param mode: specifies what to do with multiple directed edges\n" " going between the same vertex pair. C{True} or C{\"collapse\"}\n" @@ -14633,7 +14636,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_laplacian */ {"laplacian", (PyCFunction) igraphmodule_Graph_laplacian, METH_VARARGS | METH_KEYWORDS, - "laplacian(weights=None, normalized=False)\n\n" + "laplacian(weights=None, normalized=False)\n--\n\n" "Returns the Laplacian matrix of a graph.\n\n" "The Laplacian matrix is similar to the adjacency matrix, but the edges\n" "are denoted with -1 and the diagonal contains the node degrees.\n\n" @@ -14656,7 +14659,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_read_graph_dimacs {"Read_DIMACS", (PyCFunction) igraphmodule_Graph_Read_DIMACS, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_DIMACS(f, directed=False)\n\n" + "Read_DIMACS(f, directed=False)\n--\n\n" "Reads a graph from a file conforming to the DIMACS minimum-cost flow file format.\n\n" "For the exact description of the format, see\n" "U{http://lpsolve.sourceforge.net/5.5/DIMACS.htm}\n\n" @@ -14674,7 +14677,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_dl */ {"Read_DL", (PyCFunction) igraphmodule_Graph_Read_DL, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_DL(f, directed=True)\n\n" + "Read_DL(f, directed=True)\n--\n\n" "Reads an UCINET DL file and creates a graph based on it.\n\n" "@param f: the name of the file or a Python file handle\n" "@param directed: whether the generated graph should be directed.\n"}, @@ -14682,7 +14685,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_edgelist */ {"Read_Edgelist", (PyCFunction) igraphmodule_Graph_Read_Edgelist, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_Edgelist(f, directed=True)\n\n" + "Read_Edgelist(f, directed=True)\n--\n\n" "Reads an edge list from a file and creates a graph based on it.\n\n" "Please note that the vertex indices are zero-based. A vertex of zero\n" "degree will be created for every integer that is in range but does not\n" @@ -14692,7 +14695,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_graphdb */ {"Read_GraphDB", (PyCFunction) igraphmodule_Graph_Read_GraphDB, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_GraphDB(f, directed=False)\n\n" + "Read_GraphDB(f, directed=False)\n--\n\n" "Reads a GraphDB format file and creates a graph based on it.\n\n" "GraphDB is a binary format, used in the graph database for\n" "isomorphism testing (see U{http://amalfi.dis.unina.it/graph/}).\n\n" @@ -14701,7 +14704,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_graphml */ {"Read_GraphML", (PyCFunction) igraphmodule_Graph_Read_GraphML, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_GraphML(f, directed=True, index=0)\n\n" + "Read_GraphML(f, directed=True, index=0)\n--\n\n" "Reads a GraphML format file and creates a graph based on it.\n\n" "@param f: the name of the file or a Python file handle\n" "@param index: if the GraphML file contains multiple graphs,\n" @@ -14711,14 +14714,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_gml */ {"Read_GML", (PyCFunction) igraphmodule_Graph_Read_GML, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_GML(f)\n\n" + "Read_GML(f)\n--\n\n" "Reads a GML file and creates a graph based on it.\n\n" "@param f: the name of the file or a Python file handle\n" }, /* interface to igraph_read_graph_ncol */ {"Read_Ncol", (PyCFunction) igraphmodule_Graph_Read_Ncol, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_Ncol(f, names=True, weights=\"if_present\", directed=True)\n\n" + "Read_Ncol(f, names=True, weights=\"if_present\", directed=True)\n--\n\n" "Reads an .ncol file used by LGL.\n\n" "It is also useful for creating graphs from \"named\" (and\n" "optionally weighted) edge lists.\n\n" @@ -14743,7 +14746,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_lgl */ {"Read_Lgl", (PyCFunction) igraphmodule_Graph_Read_Lgl, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_Lgl(f, names=True, weights=\"if_present\", directed=True)\n\n" + "Read_Lgl(f, names=True, weights=\"if_present\", directed=True)\n--\n\n" "Reads an .lgl file used by LGL.\n\n" "It is also useful for creating graphs from \"named\" (and\n" "optionally weighted) edge lists.\n\n" @@ -14768,13 +14771,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_pajek */ {"Read_Pajek", (PyCFunction) igraphmodule_Graph_Read_Pajek, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_Pajek(f)\n\n" + "Read_Pajek(f)\n--\n\n" "Reads a Pajek format file and creates a graph based on it.\n\n" "@param f: the name of the file or a Python file handle\n"}, /* interface to igraph_write_graph_dimacs */ {"write_dimacs", (PyCFunction) igraphmodule_Graph_write_dimacs, METH_VARARGS | METH_KEYWORDS, - "write_dimacs(f, source, target, capacity=None)\n\n" + "write_dimacs(f, source, target, capacity=None)\n--\n\n" "Writes the graph in DIMACS format to the given file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" "@param source: the source vertex ID\n" @@ -14785,7 +14788,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_dot */ {"write_dot", (PyCFunction) igraphmodule_Graph_write_dot, METH_VARARGS | METH_KEYWORDS, - "write_dot(f)\n\n" + "write_dot(f)\n--\n\n" "Writes the graph in DOT format to the given file.\n\n" "DOT is the format used by the U{GraphViz }\n" "software package.\n\n" @@ -14794,14 +14797,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_edgelist */ {"write_edgelist", (PyCFunction) igraphmodule_Graph_write_edgelist, METH_VARARGS | METH_KEYWORDS, - "write_edgelist(f)\n\n" + "write_edgelist(f)\n--\n\n" "Writes the edge list of a graph to a file.\n\n" "Directed edges are written in (from, to) order.\n\n" "@param f: the name of the file to be written or a Python file handle\n"}, /* interface to igraph_write_graph_gml */ {"write_gml", (PyCFunction) igraphmodule_Graph_write_gml, METH_VARARGS | METH_KEYWORDS, - "write_gml(f, creator=None, ids=None)\n\n" + "write_gml(f, creator=None, ids=None)\n--\n\n" "Writes the graph in GML format to the given file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" "@param creator: optional creator information to be written to the file.\n" @@ -14813,7 +14816,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_ncol */ {"write_ncol", (PyCFunction) igraphmodule_Graph_write_ncol, METH_VARARGS | METH_KEYWORDS, - "write_ncol(f, names=\"name\", weights=\"weights\")\n\n" + "write_ncol(f, names=\"name\", weights=\"weights\")\n--\n\n" "Writes the edge list of a graph to a file in .ncol format.\n\n" "Note that multiple edges and/or loops break the LGL software,\n" "but igraph does not check for this condition. Unless you know\n" @@ -14829,7 +14832,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_lgl */ {"write_lgl", (PyCFunction) igraphmodule_Graph_write_lgl, METH_VARARGS | METH_KEYWORDS, - "write_lgl(f, names=\"name\", weights=\"weights\", isolates=True)\n\n" + "write_lgl(f, names=\"name\", weights=\"weights\", isolates=True)\n--\n\n" "Writes the edge list of a graph to a file in .lgl format.\n\n" "Note that multiple edges and/or loops break the LGL software,\n" "but igraph does not check for this condition. Unless you know\n" @@ -14846,21 +14849,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_write_graph_pajek */ {"write_pajek", (PyCFunction) igraphmodule_Graph_write_pajek, METH_VARARGS | METH_KEYWORDS, - "write_pajek(f)\n\n" + "write_pajek(f)\n--\n\n" "Writes the graph in Pajek format to the given file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" }, /* interface to igraph_write_graph_edgelist */ {"write_graphml", (PyCFunction) igraphmodule_Graph_write_graphml, METH_VARARGS | METH_KEYWORDS, - "write_graphml(f)\n\n" + "write_graphml(f)\n--\n\n" "Writes the graph to a GraphML file.\n\n" "@param f: the name of the file to be written or a Python file handle\n" }, /* interface to igraph_write_graph_leda */ {"write_leda", (PyCFunction) igraphmodule_Graph_write_leda, METH_VARARGS | METH_KEYWORDS, - "write_leda(f, names=\"name\", weights=\"weights\")\n\n" + "write_leda(f, names=\"name\", weights=\"weights\")\n--\n\n" "Writes the graph to a file in LEDA native format.\n\n" "The LEDA format supports at most one attribute per vertex and edge. You can\n" "specify which vertex and edge attribute you want to use. Note that the\n" @@ -14882,7 +14885,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"canonical_permutation", (PyCFunction) igraphmodule_Graph_canonical_permutation, METH_VARARGS | METH_KEYWORDS, - "canonical_permutation(sh=\"fl\", color=None)\n\n" + "canonical_permutation(sh=\"fl\", color=None)\n--\n\n" "Calculates the canonical permutation of a graph using the BLISS isomorphism\n" "algorithm.\n\n" "Passing the permutation returned here to L{Graph.permute_vertices()} will\n" @@ -14909,7 +14912,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"isoclass", (PyCFunction) igraphmodule_Graph_isoclass, METH_VARARGS | METH_KEYWORDS, - "isoclass(vertices)\n\n" + "isoclass(vertices)\n--\n\n" "Returns the isomorphism class of the graph or its subgraph.\n\n" "Isomorphy class calculations are implemented only for graphs with\n" "3 or 4 vertices.\n\n" @@ -14919,7 +14922,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the isomorphism class of the (sub)graph\n\n"}, {"isomorphic", (PyCFunction) igraphmodule_Graph_isomorphic, METH_VARARGS | METH_KEYWORDS, - "isomorphic(other)\n\n" + "isomorphic(other) -> bool\n--\n\n" "Checks whether the graph is isomorphic to another graph.\n\n" "The algorithm being used is selected using a simple heuristic:\n\n" " - If one graph is directed and the other undirected, an exception\n" @@ -14937,7 +14940,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"isomorphic_bliss", (PyCFunction) igraphmodule_Graph_isomorphic_bliss, METH_VARARGS | METH_KEYWORDS, "isomorphic_bliss(other, return_mapping_12=False, return_mapping_21=False,\n" - " sh1=\"fl\", sh2=None, color1=None, color2=None)\n\n" + " sh1=\"fl\", sh2=None, color1=None, color2=None)\n--\n\n" "Checks whether the graph is isomorphic to another graph, using the\n" "BLISS isomorphism algorithm.\n\n" "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" @@ -14977,7 +14980,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "isomorphic_vf2(other=None, color1=None, color2=None, edge_color1=None,\n" " edge_color2=None, return_mapping_12=False, return_mapping_21=False,\n" - " node_compat_fn=None, edge_compat_fn=None, callback=None)\n\n" + " node_compat_fn=None, edge_compat_fn=None, callback=None)\n--\n\n" "Checks whether the graph is isomorphic to another graph, using the\n" "VF2 isomorphism algorithm.\n\n" "Vertex and edge colors may be used to restrict the isomorphisms, as only\n" @@ -15030,7 +15033,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { (PyCFunction) igraphmodule_Graph_count_isomorphisms_vf2, METH_VARARGS | METH_KEYWORDS, "count_isomorphisms_vf2(other=None, color1=None, color2=None, edge_color1=None,\n" - " edge_color2=None, node_compat_fn=None, edge_compat_fn=None)\n\n" + " edge_color2=None, node_compat_fn=None, edge_compat_fn=None)\n--\n\n" "Determines the number of isomorphisms between the graph and another one\n\n" "Vertex and edge colors may be used to restrict the isomorphisms, as only\n" "vertices and edges with the same color will be allowed to match each other.\n\n" @@ -15102,7 +15105,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "subisomorphic_vf2(other, color1=None, color2=None, edge_color1=None,\n" " edge_color2=None, return_mapping_12=False, return_mapping_21=False,\n" - " callback=None, node_compat_fn=None, edge_compat_fn=None)\n\n" + " callback=None, node_compat_fn=None, edge_compat_fn=None)\n--\n\n" "Checks whether a subgraph of the graph is isomorphic to another graph.\n\n" "Vertex and edge colors may be used to restrict the isomorphisms, as only\n" "vertices and edges with the same color will be allowed to match each other.\n\n" @@ -15156,7 +15159,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "count_subisomorphisms_vf2(other, color1=None, color2=None,\n" " edge_color1=None, edge_color2=None, node_compat_fn=None,\n" - " edge_compat_fn=None)\n\n" + " edge_compat_fn=None)\n--\n\n" "Determines the number of subisomorphisms between the graph and another one\n\n" "Vertex and edge colors may be used to restrict the isomorphisms, as only\n" "vertices and edges with the same color will be allowed to match each other.\n\n" @@ -15191,7 +15194,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "get_subisomorphisms_vf2(other, color1=None, color2=None,\n" " edge_color1=None, edge_color2=None, node_compat_fn=None,\n" - " edge_compat_fn=None)\n\n" + " edge_compat_fn=None)\n--\n\n" "Returns all subisomorphisms between the graph and another one\n\n" "Vertex and edge colors may be used to restrict the isomorphisms, as only\n" "vertices and edges with the same color will be allowed to match each other.\n\n" @@ -15226,7 +15229,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"subisomorphic_lad", (PyCFunction) igraphmodule_Graph_subisomorphic_lad, METH_VARARGS | METH_KEYWORDS, "subisomorphic_lad(other, domains=None, induced=False, time_limit=0, \n" - " return_mapping=False)\n\n" + " return_mapping=False)\n--\n\n" "Checks whether a subgraph of the graph is isomorphic to another graph.\n\n" "The optional C{domains} argument may be used to restrict vertices that\n" "may match each other. You can also specify whether you are interested\n" @@ -15253,7 +15256,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"get_subisomorphisms_lad", (PyCFunction) igraphmodule_Graph_get_subisomorphisms_lad, METH_VARARGS | METH_KEYWORDS, - "get_subisomorphisms_lad(other, domains=None, induced=False, time_limit=0)\n\n" + "get_subisomorphisms_lad(other, domains=None, induced=False, time_limit=0)\n--\n\n" "Returns all subisomorphisms between the graph and another one using the LAD\n" "algorithm.\n\n" "The optional C{domains} argument may be used to restrict vertices that\n" @@ -15276,14 +15279,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { //////////////////////// {"attributes", (PyCFunction) igraphmodule_Graph_attributes, METH_NOARGS, - "attributes()\n\n" "@return: the attribute name list of the graph\n"}, + "attributes() -> Sequence[str]\n--\n\n" + "@return: the attribute name list of the graph\n"}, {"vertex_attributes", (PyCFunction) igraphmodule_Graph_vertex_attributes, METH_NOARGS, - "vertex_attributes()\n\n" + "vertex_attributes() -> Sequence[str]\n--\n\n" "@return: the attribute name list of the graph's vertices\n"}, {"edge_attributes", (PyCFunction) igraphmodule_Graph_edge_attributes, METH_NOARGS, - "edge_attributes()\n\n" + "edge_attributes() -> Sequence[str]\n--\n\n" "@return: the attribute name list of the graph's edges\n"}, /////////////// @@ -15291,7 +15295,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /////////////// {"complementer", (PyCFunction) igraphmodule_Graph_complementer, METH_VARARGS | METH_KEYWORDS, - "complementer(loops=False)\n\n" + "complementer(loops=False)\n--\n\n" "Returns the complementer of the graph\n\n" "@param loops: whether to include loop edges in the complementer.\n" "@return: the complementer of the graph\n"}, @@ -15306,7 +15310,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /**********************/ {"dominator", (PyCFunction) igraphmodule_Graph_dominator, METH_VARARGS | METH_KEYWORDS, - "dominator(vid, mode=)\n\n" + "dominator(vid, mode=)\n--\n\n" "Returns the dominator tree from the given root node" "@param vid: the root vertex ID\n" "@param mode: either L{IN} or L{OUT}\n" @@ -15318,7 +15322,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*****************/ {"maxflow_value", (PyCFunction) igraphmodule_Graph_maxflow_value, METH_VARARGS | METH_KEYWORDS, - "maxflow_value(source, target, capacity=None)\n\n" + "maxflow_value(source, target, capacity=None)\n--\n\n" "Returns the value of the maximum flow between the source and target vertices.\n\n" "@param source: the source vertex ID\n" "@param target: the target vertex ID\n" @@ -15329,7 +15333,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"maxflow", (PyCFunction) igraphmodule_Graph_maxflow, METH_VARARGS | METH_KEYWORDS, - "maxflow(source, target, capacity=None)\n\n" + "maxflow(source, target, capacity=None)\n--\n\n" "Returns the maximum flow between the source and target vertices.\n\n" "@attention: this function has a more convenient interface in class\n" " L{Graph} which wraps the result in a L{Flow} object. It is advised\n" @@ -15353,7 +15357,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /**********************/ {"all_st_cuts", (PyCFunction) igraphmodule_Graph_all_st_cuts, METH_VARARGS | METH_KEYWORDS, - "all_st_cuts(source, target)\n\n" + "all_st_cuts(source, target)\n--\n\n" "Returns all the cuts between the source and target vertices in a\n" "directed graph.\n\n" "This function lists all edge-cuts between a source and a target vertex.\n" @@ -15369,7 +15373,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"all_st_mincuts", (PyCFunction) igraphmodule_Graph_all_st_mincuts, METH_VARARGS | METH_KEYWORDS, - "all_st_mincuts(source, target)\n\n" + "all_st_mincuts(source, target)\n--\n\n" "Returns all minimum cuts between the source and target vertices in a\n" "directed graph.\n\n" "@param source: the source vertex ID\n" @@ -15380,7 +15384,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"mincut_value", (PyCFunction) igraphmodule_Graph_mincut_value, METH_VARARGS | METH_KEYWORDS, - "mincut_value(source=-1, target=-1, capacity=None)\n\n" + "mincut_value(source=-1, target=-1, capacity=None)\n--\n\n" "Returns the minimum cut between the source and target vertices or within\n" "the whole graph.\n\n" "@param source: the source vertex ID. If negative, the calculation is\n" @@ -15394,7 +15398,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"mincut", (PyCFunction) igraphmodule_Graph_mincut, METH_VARARGS | METH_KEYWORDS, - "mincut(source=None, target=None, capacity=None)\n\n" + "mincut(source=None, target=None, capacity=None)\n--\n\n" "Calculates the minimum cut between the source and target vertices or\n" "within the whole graph.\n\n" "The minimum cut is the minimum set of edges that needs to be removed\n" @@ -15429,7 +15433,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"st_mincut", (PyCFunction) igraphmodule_Graph_st_mincut, METH_VARARGS | METH_KEYWORDS, - "st_mincut(source, target, capacity=None)\n\n" + "st_mincut(source, target, capacity=None)\n--\n\n" "Calculates the minimum cut between the source and target vertices in a\n" "graph.\n\n" "@param source: the source vertex ID\n" @@ -15447,7 +15451,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"gomory_hu_tree", (PyCFunction) igraphmodule_Graph_gomory_hu_tree, METH_VARARGS | METH_KEYWORDS, - "gomory_hu_tree(capacity=None)\n\n" + "gomory_hu_tree(capacity=None)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.gomory_hu_tree()\n\n" }, @@ -15457,7 +15461,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*********************/ {"all_minimal_st_separators", (PyCFunction) igraphmodule_Graph_all_minimal_st_separators, METH_NOARGS, - "all_minimal_st_separators()\n\n" + "all_minimal_st_separators()\n--\n\n" "Returns a list containing all the minimal s-t separators of a graph.\n\n" "A minimal separator is a set of vertices whose removal disconnects the graph,\n" "while the removal of any subset of the set keeps the graph connected.\n\n" @@ -15471,7 +15475,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"is_minimal_separator", (PyCFunction) igraphmodule_Graph_is_minimal_separator, METH_VARARGS | METH_KEYWORDS, - "is_minimal_separator(vertices)\n\n" + "is_minimal_separator(vertices)\n--\n\n" "Decides whether the given vertex set is a minimal separator.\n\n" "A minimal separator is a set of vertices whose removal disconnects the graph,\n" "while the removal of any subset of the set keeps the graph connected.\n\n" @@ -15481,14 +15485,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"is_separator", (PyCFunction) igraphmodule_Graph_is_separator, METH_VARARGS | METH_KEYWORDS, - "is_separator(vertices)\n\n" + "is_separator(vertices)\n--\n\n" "Decides whether the removal of the given vertices disconnects the graph.\n\n" "@param vertices: a single vertex ID or a list of vertex IDs\n" "@return: C{True} is the given vertex set is a separator, C{False} if not.\n"}, {"minimum_size_separators", (PyCFunction) igraphmodule_Graph_minimum_size_separators, METH_NOARGS, - "minimum_size_separators()\n\n" + "minimum_size_separators()\n--\n\n" "Returns a list containing all separator vertex sets of minimum size.\n\n" "A vertex set is a separator if its removal disconnects the graph. This method\n" "lists all the separators for which no smaller separator set exists in the\n" @@ -15504,7 +15508,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*******************/ {"cohesive_blocks", (PyCFunction) igraphmodule_Graph_cohesive_blocks, METH_NOARGS, - "cohesive_blocks()\n\n" + "cohesive_blocks()\n--\n\n" "Calculates the cohesive block structure of the graph.\n\n" "@attention: this function has a more convenient interface in class\n" " L{Graph} which wraps the result in a L{CohesiveBlocks} object.\n" @@ -15516,7 +15520,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /********************************/ {"cliques", (PyCFunction) igraphmodule_Graph_cliques, METH_VARARGS | METH_KEYWORDS, - "cliques(min=0, max=0)\n\n" + "cliques(min=0, max=0)\n--\n\n" "Returns some or all cliques of the graph as a list of tuples.\n\n" "A clique is a complete subgraph -- a set of vertices where an edge\n" "is present between any two of them (excluding loops)\n\n" @@ -15526,7 +15530,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " negative, no upper bound will be used."}, {"largest_cliques", (PyCFunction) igraphmodule_Graph_largest_cliques, METH_NOARGS, - "largest_cliques()\n\n" + "largest_cliques()\n--\n\n" "Returns the largest cliques of the graph as a list of tuples.\n\n" "Quite intuitively a clique is considered largest if there is no clique\n" "with more vertices in the whole graph. All largest cliques are maximal\n" @@ -15535,7 +15539,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " L{maximal_cliques()} for the maximal cliques"}, {"maximal_cliques", (PyCFunction) igraphmodule_Graph_maximal_cliques, METH_VARARGS | METH_KEYWORDS, - "maximal_cliques(min=0, max=0, file=None)\n\n" + "maximal_cliques(min=0, max=0, file=None)\n--\n\n" "Returns the maximal cliques of the graph as a list of tuples.\n\n" "A maximal clique is a clique which can't be extended by adding any other\n" "vertex to it. A maximal clique is not necessarily one of the largest\n" @@ -15554,14 +15558,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@see: L{largest_cliques()} for the largest cliques."}, {"clique_number", (PyCFunction) igraphmodule_Graph_clique_number, METH_NOARGS, - "clique_number()\n\n" + "clique_number() -> int\n--\n\n" "Returns the clique number of the graph.\n\n" "The clique number of the graph is the size of the largest clique.\n\n" "@see: L{largest_cliques()} for the largest cliques."}, {"independent_vertex_sets", (PyCFunction) igraphmodule_Graph_independent_vertex_sets, METH_VARARGS | METH_KEYWORDS, - "independent_vertex_sets(min=0, max=0)\n\n" + "independent_vertex_sets(min=0, max=0)\n--\n\n" "Returns some or all independent vertex sets of the graph as a list of tuples.\n\n" "Two vertices are independent if there is no edge between them. Members\n" "of an independent vertex set are mutually independent.\n\n" @@ -15572,7 +15576,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"largest_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_largest_independent_vertex_sets, METH_NOARGS, - "largest_independent_vertex_sets()\n\n" + "largest_independent_vertex_sets()\n--\n\n" "Returns the largest independent vertex sets of the graph as a list of tuples.\n\n" "Quite intuitively an independent vertex set is considered largest if\n" "there is no other set with more vertices in the whole graph. All largest\n" @@ -15584,7 +15588,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"maximal_independent_vertex_sets", (PyCFunction) igraphmodule_Graph_maximal_independent_vertex_sets, METH_NOARGS, - "maximal_independent_vertex_sets()\n\n" + "maximal_independent_vertex_sets()\n--\n\n" "Returns the maximal independent vertex sets of the graph as a list of tuples.\n\n" "A maximal independent vertex set is an independent vertex set\n" "which can't be extended by adding any other vertex to it. A maximal\n" @@ -15599,7 +15603,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"independence_number", (PyCFunction) igraphmodule_Graph_independence_number, METH_NOARGS, - "independence_number()\n\n" + "independence_number() -> int\n--\n\n" "Returns the independence number of the graph.\n\n" "The independence number of the graph is the size of the largest\n" "independent vertex set.\n\n" @@ -15611,7 +15615,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*********************************/ {"modularity", (PyCFunction) igraphmodule_Graph_modularity, METH_VARARGS | METH_KEYWORDS, - "modularity(membership, weights=None, resolution=1, directed=True)\n\n" + "modularity(membership, weights=None, resolution=1, directed=True) -> float\n--\n\n" "Calculates the modularity of the graph with respect to some vertex types.\n\n" "The modularity of a graph w.r.t. some division measures how good the\n" "division is, or how separated are the different vertex types from each\n" @@ -15649,7 +15653,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"coreness", (PyCFunction) igraphmodule_Graph_coreness, METH_VARARGS | METH_KEYWORDS, - "coreness(mode=ALL)\n\n" + "coreness(mode=ALL) -> Sequence[int]\n--\n\n" "Finds the coreness (shell index) of the vertices of the network.\n\n" "The M{k}-core of a graph is a maximal subgraph in which each vertex\n" "has at least degree k. (Degree here means the degree in the\n" @@ -15665,7 +15669,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_fastgreedy", (PyCFunction) igraphmodule_Graph_community_fastgreedy, METH_VARARGS | METH_KEYWORDS, - "community_fastgreedy(weights=None)\n\n" + "community_fastgreedy(weights=None)\n--\n\n" "Finds the community structure of the graph according to the algorithm of\n" "Clauset et al based on the greedy optimization of modularity.\n\n" "This is a bottom-up algorithm: initially every vertex belongs to a separate\n" @@ -15688,7 +15692,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_infomap", (PyCFunction) igraphmodule_Graph_community_infomap, METH_VARARGS | METH_KEYWORDS, - "community_infomap(edge_weights=None, vertex_weights=None, trials=10)\n\n" + "community_infomap(edge_weights=None, vertex_weights=None, trials=10)\n--\n\n" "Finds the community structure of the network according to the Infomap\n" "method of Martin Rosvall and Carl T. Bergstrom.\n\n" "See U{http://www.mapequation.org} for a visualization of the algorithm\n" @@ -15711,7 +15715,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_label_propagation", (PyCFunction) igraphmodule_Graph_community_label_propagation, METH_VARARGS | METH_KEYWORDS, - "community_label_propagation(weights=None, initial=None, fixed=None)\n\n" + "community_label_propagation(weights=None, initial=None, fixed=None)\n--\n\n" "Finds the community structure of the graph according to the label\n" "propagation method of Raghavan et al.\n\n" "Initially, each vertex is assigned a different label. After that,\n" @@ -15744,7 +15748,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"community_leading_eigenvector", (PyCFunction) igraphmodule_Graph_community_leading_eigenvector, METH_VARARGS | METH_KEYWORDS, - "community_leading_eigenvector(n=-1, arpack_options=None, weights=None)\n\n" + "community_leading_eigenvector(n=-1, arpack_options=None, weights=None)\n--\n\n" "A proper implementation of Newman's eigenvector community structure\n" "detection. Each split is done by maximizing the modularity regarding\n" "the original network. See the reference for details.\n\n" @@ -15768,7 +15772,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_multilevel", (PyCFunction) igraphmodule_Graph_community_multilevel, METH_VARARGS | METH_KEYWORDS, - "community_multilevel(weights=None, return_levels=True, resolution=1)\n\n" + "community_multilevel(weights=None, return_levels=True, resolution=1)\n--\n\n" "Finds the community structure of the graph according to the multilevel\n" "algorithm of Blondel et al. This is a bottom-up algorithm: initially\n" "every vertex belongs to a separate community, and vertices are moved\n" @@ -15804,7 +15808,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_edge_betweenness", (PyCFunction)igraphmodule_Graph_community_edge_betweenness, METH_VARARGS | METH_KEYWORDS, - "community_edge_betweenness(directed=True, weights=None)\n\n" + "community_edge_betweenness(directed=True, weights=None)\n--\n\n" "Community structure detection based on the betweenness of the edges in\n" "the network. This algorithm was invented by M Girvan and MEJ Newman,\n" "see: M Girvan and MEJ Newman: Community structure in social and biological\n" @@ -15826,7 +15830,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_optimal_modularity", (PyCFunction) igraphmodule_Graph_community_optimal_modularity, METH_VARARGS | METH_KEYWORDS, - "community_optimal_modularity(weights=None)\n\n" + "community_optimal_modularity(weights=None)\n--\n\n" "Calculates the optimal modularity score of the graph and the\n" "corresponding community structure.\n\n" "This function uses the GNU Linear Programming Kit to solve a large\n" @@ -15845,7 +15849,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "community_spinglass(weights=None, spins=25, parupdate=False, " "start_temp=1, stop_temp=0.01, cool_fact=0.99, update_rule=\"config\", " - "gamma=1, implementation=\"orig\", lambda=1)\n\n" + "gamma=1, implementation=\"orig\", lambda=1)\n--\n\n" "Finds the community structure of the graph according to the spinglass\n" "community detection method of Reichardt & Bornholdt.\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" @@ -15883,35 +15887,35 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"community_leiden", (PyCFunction) igraphmodule_Graph_community_leiden, METH_VARARGS | METH_KEYWORDS, - "community_leiden(edge_weights=None, node_weights=None, \n" - " resolution_parameter=1.0, normalize_resolution=False, beta=0.01, \n" - " initial_membership=None, n_iterations=2)\n\n" - " Finds the community structure of the graph using the\n" - " Leiden algorithm of Traag, van Eck & Waltman \n\n" - " @param edge_weights: edge weights to be used. Can be a sequence or\n" - " iterable or even an edge attribute name.\n" - " @param node_weights: the node weights used in the Leiden algorithm.\n" - " @param resolution_parameter: the resolution parameter to use.\n" - " Higher resolutions lead to more smaller communities, while \n" - " lower resolutions lead to fewer larger communities.\n" - " @param normalize_resolution: if set to true, the resolution parameter\n" - " will be divided by the sum of the node weights. If this is not\n" - " supplied, it will default to the node degree, or weighted degree\n" - " in case edge_weights are supplied.\n" - " @param node_weights: the node weights used in the Leiden algorithm.\n" - " @param beta: parameter affecting the randomness in the Leiden \n" - " algorithm. This affects only the refinement step of the algorithm.\n" - " @param initial_membership: if provided, the Leiden algorithm\n" - " will try to improve this provided membership. If no argument is\n" - " provided, the aglorithm simply starts from the singleton partition.\n" - " @param n_iterations: the number of iterations to iterate the Leiden\n" - " algorithm. Each iteration may improve the partition further.\n" - " @return: the community membership vector.\n" + "community_leiden(edge_weights=None, node_weights=None, " + "resolution_parameter=1.0, normalize_resolution=False, beta=0.01, " + "initial_membership=None, n_iterations=2)\n--\n\n" + "Finds the community structure of the graph using the Leiden algorithm of\n" + "Traag, van Eck & Waltman.\n\n" + "@param edge_weights: edge weights to be used. Can be a sequence or\n" + " iterable or even an edge attribute name.\n" + "@param node_weights: the node weights used in the Leiden algorithm.\n" + "@param resolution_parameter: the resolution parameter to use.\n" + " Higher resolutions lead to more smaller communities, while \n" + " lower resolutions lead to fewer larger communities.\n" + "@param normalize_resolution: if set to true, the resolution parameter\n" + " will be divided by the sum of the node weights. If this is not\n" + " supplied, it will default to the node degree, or weighted degree\n" + " in case edge_weights are supplied.\n" + "@param node_weights: the node weights used in the Leiden algorithm.\n" + "@param beta: parameter affecting the randomness in the Leiden \n" + " algorithm. This affects only the refinement step of the algorithm.\n" + "@param initial_membership: if provided, the Leiden algorithm\n" + " will try to improve this provided membership. If no argument is\n" + " provided, the aglorithm simply starts from the singleton partition.\n" + "@param n_iterations: the number of iterations to iterate the Leiden\n" + " algorithm. Each iteration may improve the partition further.\n" + "@return: the community membership vector.\n" }, {"community_walktrap", (PyCFunction) igraphmodule_Graph_community_walktrap, METH_VARARGS | METH_KEYWORDS, - "community_walktrap(weights=None, steps=None)\n\n" + "community_walktrap(weights=None, steps=None)\n--\n\n" "Finds the community structure of the graph according to the random walk\n" "method of Latapy & Pons.\n\n" "The basic idea of the algorithm is that short random walks tend to stay\n" @@ -15934,18 +15938,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*************/ {"_is_matching", (PyCFunction)igraphmodule_Graph_is_matching, METH_VARARGS | METH_KEYWORDS, - "_is_matching(matching, types=None)\n\n" + "_is_matching(matching, types=None)\n--\n\n" "Internal function, undocumented.\n\n" }, {"_is_maximal_matching", (PyCFunction)igraphmodule_Graph_is_maximal_matching, METH_VARARGS | METH_KEYWORDS, - "_is_maximal_matching(matching, types=None)\n\n" + "_is_maximal_matching(matching, types=None)\n--\n\n" "Internal function, undocumented.\n\n" "Use L{Matching.is_maximal} instead.\n" }, {"_maximum_bipartite_matching", (PyCFunction)igraphmodule_Graph_maximum_bipartite_matching, METH_VARARGS | METH_KEYWORDS, - "_maximum_bipartite_matching(types, weights=None)\n\n" + "_maximum_bipartite_matching(types, weights=None)\n--\n\n" "Internal function, undocumented.\n\n" "@see: L{Graph.maximum_bipartite_matching}\n" }, @@ -15955,7 +15959,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /****************/ {"random_walk", (PyCFunction)igraphmodule_Graph_random_walk, METH_VARARGS | METH_KEYWORDS, - "random_walk(start, steps, mode=\"out\", stuck=\"return\")\n\n" + "random_walk(start, steps, mode=\"out\", stuck=\"return\")\n--\n\n" "Performs a random walk of a given length from a given node.\n\n" "@param start: the starting vertex of the walk\n" "@param steps: the number of steps that the random walk should take\n" @@ -15986,7 +15990,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"__graph_as_cobject", (PyCFunction) igraphmodule_Graph___graph_as_cobject__, METH_VARARGS | METH_KEYWORDS, - "__graph_as_cobject()\n\n" + "__graph_as_cobject()\n--\n\n" "Returns the igraph graph encapsulated by the Python object as\n" "a PyCObject\n\n." "A PyCObject is practically a regular C pointer, wrapped in a\n" @@ -15998,7 +16002,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"_raw_pointer", (PyCFunction) igraphmodule_Graph__raw_pointer, METH_NOARGS, - "_raw_pointer()\n\n" + "_raw_pointer() -> int\n--\n\n" "Returns the memory address of the igraph graph encapsulated by the Python\n" "object as an ordinary Python integer.\n\n" "This function should not be used directly by igraph users, it is useful\n" @@ -16008,7 +16012,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"__register_destructor", (PyCFunction) igraphmodule_Graph___register_destructor__, METH_VARARGS | METH_KEYWORDS, - "__register_destructor(destructor)\n\n" + "__register_destructor(destructor) -> None\n--\n\n" "Registers a destructor to be called when the object is freed by\n" "Python. This function should not be used directly by igraph users."}, @@ -16090,7 +16094,7 @@ PyNumberMethods igraphmodule_Graph_as_number = { */ PyTypeObject igraphmodule_GraphType = { PyVarObject_HEAD_INIT(0, 0) - "igraph.Graph", /* tp_name */ + "igraph._igraph.GraphBase", /* tp_name */ sizeof(igraphmodule_GraphObject), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor) igraphmodule_Graph_dealloc, /* tp_dealloc */ @@ -16117,9 +16121,6 @@ PyTypeObject igraphmodule_GraphType = { Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */ "Low-level representation of a graph.\n\n" "Don't use it directly, use L{igraph.Graph} instead.\n\n" - "@undocumented: _Bipartite, _Full_Bipartite, _GRG, _Incidence, _is_matching,\n" - " _is_maximal_matching, _layout_sugiyama, _maximum_bipartite_matching,\n" - " _spanning_tree\n" "@deffield ref: Reference", /* tp_doc */ (traverseproc) igraphmodule_Graph_traverse, /* tp_traverse */ (inquiry) igraphmodule_Graph_clear, /* tp_clear */ diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 86cae3747..51091ccc5 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -606,19 +606,19 @@ static PyMethodDef igraphmodule_methods[] = { {"community_to_membership", (PyCFunction)igraphmodule_community_to_membership, METH_VARARGS | METH_KEYWORDS, - "community_to_membership(merges, nodes, steps, return_csize=False)" + "community_to_membership(merges, nodes, steps, return_csize=False)\n--\n\n" }, {"_compare_communities", (PyCFunction)igraphmodule_compare_communities, METH_VARARGS | METH_KEYWORDS, - "_compare_communities(comm1, comm2, method=\"vi\")" + "_compare_communities(comm1, comm2, method=\"vi\")\n--\n\n" }, {"_power_law_fit", (PyCFunction)igraphmodule_power_law_fit, METH_VARARGS | METH_KEYWORDS, - "_power_law_fit(data, xmin=-1, force_continuous=False)" + "_power_law_fit(data, xmin=-1, force_continuous=False)\n--\n\n" }, {"convex_hull", (PyCFunction)igraphmodule_convex_hull, METH_VARARGS | METH_KEYWORDS, - "convex_hull(vs, coords=False)\n\n" + "convex_hull(vs, coords=False)\n--\n\n" "Calculates the convex hull of a given point set.\n\n" "@param vs: the point set as a list of lists\n" "@param coords: if C{True}, the function returns the\n" @@ -630,7 +630,7 @@ static PyMethodDef igraphmodule_methods[] = }, {"is_degree_sequence", (PyCFunction)igraphmodule_is_degree_sequence, METH_VARARGS | METH_KEYWORDS, - "is_degree_sequence(out_deg, in_deg=None)\n\n" + "is_degree_sequence(out_deg, in_deg=None)\n--\n\n" "Deprecated since 0.9 in favour of L{is_graphical()}.\n\n" "Returns whether a list of degrees can be a degree sequence of some graph.\n\n" "Note that it is not required for the graph to be simple; in other words,\n" @@ -650,7 +650,7 @@ static PyMethodDef igraphmodule_methods[] = }, {"is_graphical", (PyCFunction)igraphmodule_is_graphical, METH_VARARGS | METH_KEYWORDS, - "is_graphical(out_deg, in_deg=None, loops=False, multiple=False)\n\n" + "is_graphical(out_deg, in_deg=None, loops=False, multiple=False)\n--\n\n" "Returns whether a list of degrees can be a degree sequence of some graph,\n" "with or without multiple and loop edges, depending on the allowed edge types\n" "in the remaining arguments.\n\n" @@ -665,7 +665,7 @@ static PyMethodDef igraphmodule_methods[] = }, {"is_graphical_degree_sequence", (PyCFunction)igraphmodule_is_graphical_degree_sequence, METH_VARARGS | METH_KEYWORDS, - "is_graphical_degree_sequence(out_deg, in_deg=None)\n\n" + "is_graphical_degree_sequence(out_deg, in_deg=None)\n--\n\n" "Deprecated since 0.9 in favour of L{is_graphical()}.\n\n" "Returns whether a list of degrees can be a degree sequence of some simple graph.\n\n" "Note that it is required for the graph to be simple; in other words,\n" @@ -679,7 +679,7 @@ static PyMethodDef igraphmodule_methods[] = " degree sequence, C{False} otherwise.\n" }, {"set_progress_handler", igraphmodule_set_progress_handler, METH_O, - "set_progress_handler(handler)\n\n" + "set_progress_handler(handler)\n--\n\n" "Sets the handler to be called when igraph is performing a long operation.\n" "@param handler: the progress handler function. It must accept two\n" " arguments, the first is the message informing the user about\n" @@ -687,7 +687,7 @@ static PyMethodDef igraphmodule_methods[] = " progress information (a percentage).\n" }, {"set_random_number_generator", igraph_rng_Python_set_generator, METH_O, - "set_random_number_generator(generator)\n\n" + "set_random_number_generator(generator)\n--\n\n" "Sets the random number generator used by igraph.\n" "@param generator: the generator to be used. It must be a Python object\n" " with at least three attributes: C{random}, C{randint} and C{gauss}.\n" @@ -701,7 +701,7 @@ static PyMethodDef igraphmodule_methods[] = " for random numbers, but you cannot set its seed or save its state.\n" }, {"set_status_handler", igraphmodule_set_status_handler, METH_O, - "set_status_handler(handler)\n\n" + "set_status_handler(handler)\n--\n\n" "Sets the handler to be called when igraph tries to display a status\n" "message.\n\n" "This is used to communicate the progress of some calculations where\n" @@ -713,28 +713,26 @@ static PyMethodDef igraphmodule_methods[] = }, {"_split_join_distance", (PyCFunction)igraphmodule_split_join_distance, METH_VARARGS | METH_KEYWORDS, - "_split_join_distance(comm1, comm2)" + "_split_join_distance(comm1, comm2)\n--\n\n" }, {"_disjoint_union", (PyCFunction)igraphmodule__disjoint_union, METH_VARARGS | METH_KEYWORDS, - "_disjoint_union(graphs)" + "_disjoint_union(graphs)\n--\n\n" }, {"_union", (PyCFunction)igraphmodule__union, METH_VARARGS | METH_KEYWORDS, - "_union(graphs, edgemaps)" + "_union(graphs, edgemaps)\n--\n\n" }, {"_intersection", (PyCFunction)igraphmodule__intersection, METH_VARARGS | METH_KEYWORDS, - "_intersection(graphs, edgemaps)" + "_intersection(graphs, edgemaps)\n--\n\n" }, {NULL, NULL, 0, NULL} }; #define MODULE_DOCS \ "Low-level Python interface for the igraph library. " \ - "Should not be used directly.\n\n" \ - "@undocumented: community_to_membership, _compare_communities, _power_law_fit, " \ - "_split_join_distance, _union, _intersection, _disjoint_union" + "Should not be used directly.\n" /** * Module definition table (only for Python 3.x) diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 834541834..138a225b1 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -750,17 +750,17 @@ GRAPH_PROXY_METHOD_PP(successors, "successors", _convert_to_vertex_list); PyMethodDef igraphmodule_Vertex_methods[] = { {"attributes", (PyCFunction)igraphmodule_Vertex_attributes, METH_NOARGS, - "attributes() -> dict\n\n" + "attributes() -> dict\n--\n\n" "Returns a dict of attribute names and values for the vertex\n" }, {"attribute_names", (PyCFunction)igraphmodule_Vertex_attribute_names, METH_NOARGS, - "attribute_names() -> list\n\n" + "attribute_names() -> list\n--\n\n" "Returns the list of vertex attribute names\n" }, {"update_attributes", (PyCFunction)igraphmodule_Vertex_update_attributes, METH_VARARGS | METH_KEYWORDS, - "update_attributes(E, **F) -> None\n\n" + "update_attributes(E, **F) -> None\n--\n\n" "Updates the attributes of the vertex from dict/iterable E and F.\n\n" "If E has a C{keys()} method, it does: C{for k in E: self[k] = E[k]}.\n" "If E lacks a C{keys()} method, it does: C{for (k, v) in E: self[k] = v}.\n" diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 533366e37..88e476e7d 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -899,17 +899,17 @@ PyObject* igraphmodule_VertexSeq__reindex_names(igraphmodule_VertexSeqObject* se PyMethodDef igraphmodule_VertexSeq_methods[] = { {"attribute_names", (PyCFunction)igraphmodule_VertexSeq_attribute_names, METH_NOARGS, - "attribute_names() -> list\n\n" + "attribute_names() -> list\n--\n\n" "Returns the attribute name list of the graph's vertices\n" }, {"find", (PyCFunction)igraphmodule_VertexSeq_find, METH_VARARGS, - "find(condition) -> Vertex\n\n" + "find(condition) -> Vertex\n--\n\n" "For internal use only.\n" }, {"get_attribute_values", (PyCFunction)igraphmodule_VertexSeq_get_attribute_values, METH_O, - "get_attribute_values(attrname) -> list\n" + "get_attribute_values(attrname) -> list\n--\n\n" "Returns the value of a given vertex attribute for all vertices in a list.\n\n" "The values stored in the list are exactly the same objects that are stored\n" "in the vertex attribute, meaning that in the case of mutable objects,\n" @@ -920,17 +920,18 @@ PyMethodDef igraphmodule_VertexSeq_methods[] = { }, {"set_attribute_values", (PyCFunction)igraphmodule_VertexSeq_set_attribute_values, METH_VARARGS | METH_KEYWORDS, - "set_attribute_values(attrname, values) -> list\n" + "set_attribute_values(attrname, values) -> list\n--\n\n" "Sets the value of a given vertex attribute for all vertices\n\n" "@param attrname: the name of the attribute\n" "@param values: the new attribute values in a list\n" }, {"select", (PyCFunction)igraphmodule_VertexSeq_select, METH_VARARGS, - "select(...) -> VertexSeq\n\n" + "select(...) -> VertexSeq\n--\n\n" "For internal use only.\n" }, {"_reindex_names", (PyCFunction)igraphmodule_VertexSeq__reindex_names, METH_NOARGS, + "_reindex_names() -> None\n--\n\n" "Re-creates the dictionary that maps vertex names to IDs.\n\n" "For internal use only.\n" }, diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 9d10f92c0..37a45534c 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2,9 +2,6 @@ # -*- coding: utf-8 -*- """ IGraph library. - -@undocumented: deprecated, _graphmethod, _add_proxy_methods, _layout_method_wrapper, - _3d_version_for """ @@ -163,7 +160,7 @@ class Graph(GraphBase): """Generic graph. This class is built on top of L{GraphBase}, so the order of the - methods in the Epydoc documentation is a little bit obscure: + methods in the generated API documentation is a little bit obscure: inherited methods come after the ones implemented directly in the subclass. L{Graph} provides many functions that L{GraphBase} does not, mostly because these functions are not speed critical and they were diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 4217f3dce..f1b57e345 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -73,8 +73,6 @@ class Clustering(object): >>> cluster_list = list(cl) >>> print(cluster_list) [[0, 1, 2, 3], [4, 5, 6], [7, 8, 9, 10]] - - @undocumented: _formatted_cluster_iterator """ def __init__(self, membership, params=None): @@ -226,8 +224,6 @@ class VertexClustering(Clustering): @note: since this class is linked to a L{Graph}, destroying the graph by the C{del} operator does not free the memory occupied by the graph if there exists a L{VertexClustering} that references the L{Graph}. - - @undocumented: _formatted_cluster_iterator """ # Allow None to be passed to __plot__ as the "palette" keyword argument @@ -567,8 +563,6 @@ class Dendrogram(object): 3 -+ | | | 4 -+---+--- - - @undocumented: _item_box_size, _plot_item, _traverse_inorder """ def __init__(self, merges): @@ -1119,8 +1113,6 @@ class Cover(object): >>> cover = Cover(clustering) >>> list(clustering) == list(cover) True - - @undocumented: _formatted_cluster_iterator """ def __init__(self, clusters, n=0): @@ -1247,8 +1239,6 @@ class VertexCover(Cover): @note: since this class is linked to a L{Graph}, destroying the graph by the C{del} operator does not free the memory occupied by the graph if there exists a L{VertexCover} that references the L{Graph}. - - @undocumented: _formatted_cluster_iterator """ def __init__(self, graph, clusters=None): From 3654508b83c5e20eb06bc30ddeee9687e4c03836 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 7 Mar 2021 02:01:34 +0100 Subject: [PATCH 0195/1681] fix: removed py2compat.h and other Python 2.x compatibility hacks --- src/_igraph/arpackobject.c | 45 +++++---- src/_igraph/attributes.c | 87 ++++------------ src/_igraph/bfsiter.c | 7 +- src/_igraph/convert.c | 154 ++++++++++------------------- src/_igraph/dfsiter.c | 7 +- src/_igraph/edgeobject.c | 36 ++----- src/_igraph/edgeobject.h | 3 +- src/_igraph/edgeseqobject.c | 17 ++-- src/_igraph/filehandle.c | 180 +--------------------------------- src/_igraph/graphobject.c | 97 +++++------------- src/_igraph/igraphmodule.c | 30 +----- src/_igraph/indexing.c | 9 +- src/_igraph/py2compat.c | 146 --------------------------- src/_igraph/py2compat.h | 94 ------------------ src/_igraph/pyhelpers.c | 87 +++++++++++++--- src/_igraph/pyhelpers.h | 8 ++ src/_igraph/random.c | 3 +- src/_igraph/vertexobject.c | 37 ++----- src/_igraph/vertexobject.h | 3 +- src/_igraph/vertexseqobject.c | 19 ++-- 20 files changed, 258 insertions(+), 811 deletions(-) delete mode 100644 src/_igraph/py2compat.c delete mode 100644 src/_igraph/py2compat.h diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 96eefa340..918a2f697 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -23,7 +23,6 @@ #include "arpackobject.h" #include "graphobject.h" #include "error.h" -#include "py2compat.h" PyObject* igraphmodule_arpack_options_default; @@ -70,45 +69,45 @@ PyObject* igraphmodule_ARPACKOptions_getattr( if (strcmp(attrname, "bmat") == 0) { char buf[2] = { self->params_out.bmat[0], 0 }; - result=PyString_FromString(buf); + result=PyUnicode_FromString(buf); } else if (strcmp(attrname, "n") == 0) { - result=PyInt_FromLong(self->params_out.n); + result=PyLong_FromLong(self->params_out.n); } else if (strcmp(attrname, "which") == 0) { char buf[3] = { self->params.which[0], self->params.which[1], 0 }; - result=PyString_FromString(buf); + result=PyUnicode_FromString(buf); } else if (strcmp(attrname, "nev") == 0) { - result=PyInt_FromLong(self->params.nev); + result=PyLong_FromLong(self->params.nev); } else if (strcmp(attrname, "tol") == 0) { result=PyFloat_FromDouble((double)self->params.tol); } else if (strcmp(attrname, "ncv") == 0) { - result=PyInt_FromLong(self->params.ncv); + result=PyLong_FromLong(self->params.ncv); } else if (strcmp(attrname, "ldv") == 0) { - result=PyInt_FromLong(self->params.ldv); + result=PyLong_FromLong(self->params.ldv); } else if (strcmp(attrname, "ishift") == 0) { - result=PyInt_FromLong(self->params.ishift); + result=PyLong_FromLong(self->params.ishift); } else if (strcmp(attrname, "maxiter") == 0 || strcmp(attrname, "mxiter") == 0) { - result=PyInt_FromLong(self->params.mxiter); + result=PyLong_FromLong(self->params.mxiter); } else if (strcmp(attrname, "nb") == 0) { - result=PyInt_FromLong(self->params.nb); + result=PyLong_FromLong(self->params.nb); } else if (strcmp(attrname, "mode") == 0) { - result=PyInt_FromLong(self->params.mode); + result=PyLong_FromLong(self->params.mode); } else if (strcmp(attrname, "start") == 0) { - result=PyInt_FromLong(self->params.start); + result=PyLong_FromLong(self->params.start); } else if (strcmp(attrname, "sigma") == 0) { result=PyFloat_FromDouble((double)self->params.sigma); } else if (strcmp(attrname, "info") == 0) { - result=PyInt_FromLong(self->params_out.info); + result=PyLong_FromLong(self->params_out.info); } else if (strcmp(attrname, "iter") == 0) { - result=PyInt_FromLong(self->params_out.iparam[2]); + result=PyLong_FromLong(self->params_out.iparam[2]); } else if (strcmp(attrname, "nconv") == 0) { - result=PyInt_FromLong(self->params_out.iparam[4]); + result=PyLong_FromLong(self->params_out.iparam[4]); } else if (strcmp(attrname, "numop") == 0) { - result=PyInt_FromLong(self->params_out.iparam[8]); + result=PyLong_FromLong(self->params_out.iparam[8]); } else if (strcmp(attrname, "numopb") == 0) { - result=PyInt_FromLong(self->params_out.iparam[9]); + result=PyLong_FromLong(self->params_out.iparam[9]); } else if (strcmp(attrname, "numreo") == 0) { - result=PyInt_FromLong(self->params_out.iparam[10]); + result=PyLong_FromLong(self->params_out.iparam[10]); } else { PyErr_SetString(PyExc_AttributeError, attrname); } @@ -127,8 +126,8 @@ int igraphmodule_ARPACKOptions_setattr( } if (strcmp(attrname, "maxiter") == 0 || strcmp(attrname, "mxiter") == 0) { - if (PyInt_Check(value)) { - long int n=PyInt_AsLong(value); + if (PyLong_Check(value)) { + long int n=PyLong_AsLong(value); if (n>0) self->params.mxiter=(igraph_integer_t)n; else { @@ -140,8 +139,8 @@ int igraphmodule_ARPACKOptions_setattr( return -1; } } else if (strcmp(attrname, "tol") == 0) { - if (PyInt_Check(value)) { - self->params.tol = (igraph_real_t) PyInt_AsLong(value); + if (PyLong_Check(value)) { + self->params.tol = (igraph_real_t) PyLong_AsLong(value); } else if (PyFloat_Check(value)) { self->params.tol = (igraph_real_t) PyFloat_AsDouble(value); } else { @@ -180,7 +179,7 @@ PyObject* igraphmodule_ARPACKOptions_str( igraphmodule_ARPACKOptionsObject *self) { PyObject *s; - s=PyString_FromFormat("ARPACK parameters"); + s=PyUnicode_FromFormat("ARPACK parameters"); return s; } diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 4090bc04f..8a8cf79a5 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -24,7 +24,6 @@ #include "attributes.h" #include "common.h" #include "convert.h" -#include "py2compat.h" #include "pyhelpers.h" int igraphmodule_i_attribute_struct_init(igraphmodule_i_attribute_struct *attrs) { @@ -76,7 +75,7 @@ int igraphmodule_i_attribute_struct_index_vertex_names( n = PyList_Size(name_list) - 1; while (n >= 0) { key = PyList_GET_ITEM(name_list, n); /* we don't own a reference to key */ - value = PyInt_FromLong(n); /* we do own a reference to value */ + value = PyLong_FromLong(n); /* we do own a reference to value */ if (value == 0) return 1; @@ -88,10 +87,7 @@ int igraphmodule_i_attribute_struct_index_vertex_names( PyExc_RuntimeError, "error while indexing vertex names; did you accidentally try to " "use a non-hashable object as a vertex name earlier?" -#ifdef IGRAPH_PYTHON3 - /* %R is not supported in Python 2.x */ " Check the name of vertex %R (%R)", value, key -#endif ); } @@ -126,33 +122,13 @@ void igraphmodule_index_vertex_names(igraph_t *graph, igraph_bool_t force) { } int igraphmodule_PyObject_matches_attribute_record(PyObject* object, igraph_attribute_record_t* record) { -#ifndef IGRAPH_PYTHON3 - int result; -#endif - if (record == 0) { return 0; } - if (PyString_Check(object)) { - return PyString_IsEqualToASCIIString(object, record->name); - } - -#ifndef IGRAPH_PYTHON3 - /* On Python 2.x, we need to handle Unicode strings as well because - * the user might use 'from __future__ import unicode_literals', which - * would turn some igraph attribute names into Unicode strings */ if (PyUnicode_Check(object)) { - PyObject* ascii = PyUnicode_AsASCIIString(object); - if (ascii == 0) { - return 0; - } - - result = PyString_IsEqualToASCIIString(ascii, record->name); - Py_DECREF(ascii); - return result; + return PyUnicode_IsEqualToASCIIString(object, record->name); } -#endif return 0; } @@ -170,21 +146,11 @@ int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_inte } if (o_vid == NULL) { -#ifdef IGRAPH_PYTHON3 PyErr_Format(PyExc_ValueError, "no such vertex: %R", o); -#else - PyObject* s = PyObject_Repr(o); - if (s) { - PyErr_Format(PyExc_ValueError, "no such vertex: %s", PyString_AS_STRING(s)); - Py_DECREF(s); - } else { - PyErr_Format(PyExc_ValueError, "no such vertex: %p", o); - } -#endif return 1; } - if (!PyInt_Check(o_vid)) { + if (!PyLong_Check(o_vid)) { PyErr_SetString(PyExc_ValueError, "non-numeric vertex ID assigned to vertex name. This is most likely a bug."); return 1; } @@ -350,9 +316,9 @@ static int igraphmodule_i_attribute_init(igraph_t *graph, igraph_vector_ptr_t *a case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, 0, &s); if (s == 0) - value=PyString_FromString(""); + value=PyUnicode_FromString(""); else - value=PyString_FromString(s); + value=PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: value=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[0] ? Py_True : Py_False; @@ -498,7 +464,7 @@ static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, long int nv, i break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyString_FromString(s); + o=PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; @@ -557,7 +523,7 @@ static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, long int nv, i break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyString_FromString(s); + o=PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; @@ -679,7 +645,7 @@ static int igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vect break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyString_FromString(s); + o=PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; @@ -740,7 +706,7 @@ static int igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vect break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyString_FromString(s); + o=PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; @@ -888,11 +854,7 @@ static PyObject* igraphmodule_i_ac_builtin_func(PyObject* values, PyObject* func = 0; if (builtin_module_dict == 0) { -#ifdef IGRAPH_PYTHON3 PyObject* builtin_module = PyImport_ImportModule("builtins"); -#else - PyObject* builtin_module = PyImport_ImportModule("__builtin__"); -#endif if (builtin_module == 0) return 0; builtin_module_dict = PyModule_GetDict(builtin_module); @@ -1205,9 +1167,9 @@ static int igraphmodule_i_attribute_combine_dicts(PyObject *dict, /* Collect what to do for each attribute in the source dict */ pos = 0; i = 0; while (PyDict_Next(dict, &pos, &key, &value)) { - todo[i].name = PyString_CopyAsString(key); + todo[i].name = PyUnicode_CopyAsString(key); if (todo[i].name == 0) - IGRAPH_ERROR("PyString_CopyAsString failed", IGRAPH_FAILURE); + IGRAPH_ERROR("PyUnicode_CopyAsString failed", IGRAPH_FAILURE); igraph_attribute_combination_query(comb, todo[i].name, &todo[i].type, &todo[i].func); i++; @@ -1222,7 +1184,7 @@ static int igraphmodule_i_attribute_combine_dicts(PyObject *dict, PyObject *newvalue; /* Safety check */ - if (!PyString_IsEqualToASCIIString(key, todo[i].name)) { + if (!PyUnicode_IsEqualToASCIIString(key, todo[i].name)) { IGRAPH_ERROR("PyDict_Next iteration order not consistent. " "This should never happen. Please report the bug to the igraph " "developers!", IGRAPH_FAILURE); @@ -1276,7 +1238,7 @@ static int igraphmodule_i_attribute_combine_dicts(PyObject *dict, break; case IGRAPH_ATTRIBUTE_COMBINE_CONCAT: - empty_str = PyString_FromString(""); + empty_str = PyUnicode_FromString(""); func = PyObject_GetAttrString(empty_str, "join"); newvalue = igraphmodule_i_ac_func(value, merges, func); Py_DECREF(func); @@ -1559,7 +1521,6 @@ int igraphmodule_i_get_string_graph_attr(const igraph_t *graph, IGRAPH_ERROR("No such attribute", IGRAPH_EINVAL); IGRAPH_CHECK(igraph_strvector_resize(value, 1)); -#ifdef IGRAPH_PYTHON3 /* For Python 3.x, we simply call PyObject_Str, which produces a * Unicode string, then encode it into UTF-8, except when we * already have a PyBytes object -- this is assumed to be in @@ -1576,25 +1537,11 @@ int igraphmodule_i_get_string_graph_attr(const igraph_t *graph, Py_DECREF(unicode); } -#else - /* For Python 2.x, we check whether we have received a string or a - * Unicode string. Unicode strings are encoded into UTF-8, strings - * are used intact. - */ - if (PyUnicode_Check(o)) { - str = PyUnicode_AsEncodedString(o, "utf-8", "xmlcharrefreplace"); - } else { - str = PyObject_Str(o); - } -#endif - - if (str == 0) + if (str == 0) { IGRAPH_ERROR("Internal error in PyObject_Str", IGRAPH_EINVAL); -#ifdef IGRAPH_PYTHON3 + } + c_str = PyBytes_AS_STRING(str); -#else - c_str = PyString_AS_STRING(str); -#endif IGRAPH_CHECK(igraph_strvector_set(value, 0, c_str)); Py_XDECREF(str); @@ -1901,7 +1848,7 @@ int igraphmodule_attribute_name_check(PyObject* obj) { type_str = obj ? PyObject_Str((PyObject*)obj->ob_type) : 0; if (type_str != 0) { PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only, got %s", - PyString_AS_STRING(type_str)); + PyUnicode_AS_UNICODE(type_str)); Py_DECREF(type_str); } else { PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only"); diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 9f7e26dbd..9f7d12331 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -23,7 +23,6 @@ #include "bfsiter.h" #include "common.h" #include "error.h" -#include "py2compat.h" #include "vertexobject.h" /** @@ -50,7 +49,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, o->gref=g; o->graph=&g->g; - if (!PyInt_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { + if (!PyLong_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); return NULL; } @@ -72,8 +71,8 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, return NULL; } - if (PyInt_Check(root)) { - r=PyInt_AsLong(root); + if (PyLong_Check(root)) { + r=PyLong_AsLong(root); } else { r=((igraphmodule_VertexObject*)root)->idx; } diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 4815315ca..762bbeaf9 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -25,15 +25,15 @@ #include #include #include "attributes.h" -#include "graphobject.h" -#include "vertexseqobject.h" -#include "vertexobject.h" +#include "convert.h" #include "edgeseqobject.h" #include "edgeobject.h" -#include "convert.h" #include "error.h" +#include "graphobject.h" #include "memory.h" -#include "py2compat.h" +#include "pyhelpers.h" +#include "vertexseqobject.h" +#include "vertexobject.h" #if defined(_MSC_VER) #define strcasecmp _stricmp @@ -42,13 +42,13 @@ /** * \brief Converts a Python integer to a C int * - * This is similar to PyInt_AsLong, but it checks for overflow first and throws + * This is similar to PyLong_AsLong, but it checks for overflow first and throws * an exception if necessary. * * Returns -1 if there was an error, 0 otherwise. */ int PyInt_AsInt(PyObject* obj, int* result) { - long dummy = PyInt_AsLong(obj); + long dummy = PyLong_AsLong(obj); if (dummy < INT_MIN) { PyErr_SetString(PyExc_OverflowError, "integer too small for conversion to C int"); @@ -114,11 +114,11 @@ int igraphmodule_PyObject_to_enum(PyObject *o, if (o == 0 || o == Py_None) return 0; - if (PyInt_Check(o)) + if (PyLong_Check(o)) return PyInt_AsInt(o, result); if (PyLong_Check(o)) return PyLong_AsInt(o, result); - s = PyString_CopyAsString(o); + s = PyUnicode_CopyAsString(o); if (s == 0) { PyErr_SetString(PyExc_TypeError, "int, long or string expected"); return -1; @@ -308,7 +308,6 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, if (object != Py_None) { while (PyDict_Next(object, &pos, &key, &value)) { char *kv; -#ifdef IGRAPH_PYTHON3 PyObject *temp_bytes; if (!PyUnicode_Check(key)) { PyErr_SetString(PyExc_TypeError, "Dict key must be string"); @@ -321,45 +320,34 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, } kv = strdup(PyBytes_AS_STRING(temp_bytes)); Py_DECREF(temp_bytes); -#else - if (!PyString_Check(key)) { - PyErr_SetString(PyExc_TypeError, "Dict key must be string"); - return -1; - } - kv=PyString_AsString(key); -#endif if (!strcasecmp(kv, "pos")) { igraphmodule_PyObject_to_enum(value, eigen_which_position_tt, (int*) &w->pos); } else if (!strcasecmp(kv, "howmany")) { - w->howmany = (int) PyInt_AsLong(value); + w->howmany = (int) PyLong_AsLong(value); } else if (!strcasecmp(kv, "il")) { - w->il = (int) PyInt_AsLong(value); + w->il = (int) PyLong_AsLong(value); } else if (!strcasecmp(kv, "iu")) { - w->iu = (int) PyInt_AsLong(value); + w->iu = (int) PyLong_AsLong(value); } else if (!strcasecmp(kv, "vl")) { w->vl = PyFloat_AsDouble(value); } else if (!strcasecmp(kv, "vu")) { w->vu = PyFloat_AsDouble(value); } else if (!strcasecmp(kv, "vestimate")) { - w->vestimate = (int) PyInt_AsLong(value); + w->vestimate = (int) PyLong_AsLong(value); } else if (!strcasecmp(kv, "balance")) { igraphmodule_PyObject_to_enum(value, lapack_dgeevc_balance_tt, (int*) &w->balance); } else { PyErr_SetString(PyExc_TypeError, "Unknown eigen parameter"); -#ifdef IGRAPH_PYTHON3 if (kv != 0) { free(kv); } -#endif return -1; } -#ifdef IGRAPH_PYTHON3 if (kv != 0) { free(kv); } -#endif } } return 0; @@ -717,9 +705,8 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { return retval; *v = num; return 0; -#ifdef IGRAPH_PYTHON3 } else if (PyNumber_Check(object)) { - PyObject *i = PyNumber_Int(object); + PyObject *i = PyNumber_Long(object); if (i == NULL) return 1; retval = PyInt_AsInt(i, &num); @@ -729,25 +716,6 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { *v = num; return 0; } -#else - } else if (PyInt_Check(object)) { - retval = PyInt_AsInt(object, &num); - if (retval) - return retval; - *v = num; - return 0; - } else if (PyNumber_Check(object)) { - PyObject *i = PyNumber_Int(object); - if (i == NULL) - return 1; - retval = PyInt_AsInt(i, &num); - Py_DECREF(i); - if (retval) - return retval; - *v = num; - return 0; - } -#endif PyErr_BadArgument(); return 1; } @@ -774,12 +742,6 @@ int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v) { double d = PyLong_AsDouble(object); *v=(igraph_real_t)d; return 0; -#ifndef IGRAPH_PYTHON3 - } else if (PyInt_Check(object)) { - long l = PyInt_AS_LONG((PyIntObject*)object); - *v=(igraph_real_t)l; - return 0; -#endif } else if (PyFloat_Check(object)) { double d = PyFloat_AS_DOUBLE((PyFloatObject*)object); *v=(igraph_real_t)d; @@ -1030,7 +992,7 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v PyErr_SetString(PyExc_TypeError, "iterable must return numbers"); ok=0; } else { - PyObject *item2 = PyNumber_Int(item); + PyObject *item2 = PyNumber_Long(item); if (item2 == 0) { PyErr_SetString(PyExc_TypeError, "can't convert a list item to integer"); ok = 0; @@ -1074,7 +1036,7 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v PyErr_SetString(PyExc_TypeError, "sequence elements must be integers"); ok=0; } else { - PyObject *item2 = PyNumber_Int(item); + PyObject *item2 = PyNumber_Long(item); if (item2 == 0) { PyErr_SetString(PyExc_TypeError, "can't convert sequence element to int"); ok=0; @@ -1142,12 +1104,12 @@ int igraphmodule_PyObject_to_vector_long_t(PyObject *list, igraph_vector_long_t PyErr_SetString(PyExc_TypeError, "iterable must return numbers"); ok=0; } else { - PyObject *item2 = PyNumber_Int(item); + PyObject *item2 = PyNumber_Long(item); if (item2 == 0) { PyErr_SetString(PyExc_TypeError, "can't convert a list item to integer"); ok = 0; } else { - value=(long)PyInt_AsLong(item); + value=(long)PyLong_AsLong(item); Py_DECREF(item2); } } @@ -1186,12 +1148,12 @@ int igraphmodule_PyObject_to_vector_long_t(PyObject *list, igraph_vector_long_t PyErr_SetString(PyExc_TypeError, "sequence elements must be integers"); ok=0; } else { - PyObject *item2 = PyNumber_Int(item); + PyObject *item2 = PyNumber_Long(item); if (item2 == 0) { PyErr_SetString(PyExc_TypeError, "can't convert sequence element to integer"); ok=0; } else { - value=(long)PyInt_AsLong(item2); + value=(long)PyLong_AsLong(item2); Py_DECREF(item2); } } @@ -1325,7 +1287,7 @@ PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, if (!igraph_finite(VECTOR(*v)[i])) { item = PyFloat_FromDouble((double)VECTOR(*v)[i]); } else { - item = PyInt_FromLong((long)VECTOR(*v)[i]); + item = PyLong_FromLong((long)VECTOR(*v)[i]); } } else if (type == IGRAPHMODULE_TYPE_FLOAT) { item=PyFloat_FromDouble((double)VECTOR(*v)[i]); @@ -1360,7 +1322,7 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { list=PyList_New(n); for (i=0; iname = 0; - else if (!PyString_Check(name)) { + else if (!PyUnicode_Check(name)) { PyErr_SetString(PyExc_TypeError, "keys must be strings or None in attribute combination specification dicts"); return 1; } else { -#ifdef IGRAPH_PYTHON3 - result->name = PyString_CopyAsString(name); -#else - result->name = PyString_AS_STRING(name); -#endif + result->name = PyUnicode_CopyAsString(name); } return 0; @@ -3035,9 +2989,7 @@ int igraphmodule_PyObject_to_attribute_combination_t(PyObject* object, return 1; } igraph_attribute_combination_add(result, rec.name, rec.type, rec.func); -#ifdef IGRAPH_PYTHON3 free((char*)rec.name); /* was allocated in pair_to_attribute_combination_record_t above */ -#endif } } else { /* assume it is a string or callable */ @@ -3047,9 +2999,7 @@ int igraphmodule_PyObject_to_attribute_combination_t(PyObject* object, } igraph_attribute_combination_add(result, 0, rec.type, rec.func); -#ifdef IGRAPH_PYTHON3 free((char*)rec.name); /* was allocated in pair_to_attribute_combination_record_t above */ -#endif } return 0; diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index d7d8105af..3ea3b3ff6 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -23,7 +23,6 @@ #include "dfsiter.h" #include "common.h" #include "error.h" -#include "py2compat.h" #include "vertexobject.h" /** @@ -50,7 +49,7 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, o->gref=g; o->graph=&g->g; - if (!PyInt_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { + if (!PyLong_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); return NULL; } @@ -72,8 +71,8 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, return NULL; } - if (PyInt_Check(root)) { - r=PyInt_AsLong(root); + if (PyLong_Check(root)) { + r=PyLong_AsLong(root); } else { r = ((igraphmodule_VertexObject*)root)->idx; } diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 253f169b5..f42dfa326 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -27,7 +27,6 @@ #include "error.h" #include "graphobject.h" #include "pyhelpers.h" -#include "py2compat.h" #include "vertexobject.h" /** @@ -146,48 +145,31 @@ void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self) { PyObject *s; PyObject *attrs; -#ifndef IGRAPH_PYTHON3 - PyObject *grepr, *drepr; -#endif attrs = igraphmodule_Edge_attributes(self); if (attrs == 0) return NULL; -#ifdef IGRAPH_PYTHON3 s = PyUnicode_FromFormat("igraph.Edge(%R, %ld, %R)", (PyObject*)self->gref, (long int)self->idx, attrs); Py_DECREF(attrs); -#else - grepr=PyObject_Repr((PyObject*)self->gref); - drepr=PyObject_Repr(attrs); - Py_DECREF(attrs); - if (!grepr || !drepr) { - Py_XDECREF(grepr); - Py_XDECREF(drepr); - return NULL; - } - s=PyString_FromFormat("igraph.Edge(%s, %ld, %s)", PyString_AsString(grepr), - (long int)self->idx, PyString_AsString(drepr)); - Py_DECREF(grepr); - Py_DECREF(drepr); -#endif + return s; } /** \ingroup python_interface_edge * \brief Returns the hash code of the edge */ -Py_hash_t igraphmodule_Edge_hash(igraphmodule_EdgeObject* self) { - Py_hash_t hash_graph; - Py_hash_t hash_index; - Py_hash_t result; +long igraphmodule_Edge_hash(igraphmodule_EdgeObject* self) { + long hash_graph; + long hash_index; + long result; PyObject* index_o; if (self->hash != -1) return self->hash; - index_o = PyInt_FromLong((long int)self->idx); + index_o = PyLong_FromLong((long int)self->idx); if (index_o == 0) return -1; @@ -447,7 +429,7 @@ PyObject* igraphmodule_Edge_get_from(igraphmodule_EdgeObject* self, void* closur if (igraph_edge(&o->g, self->idx, &from, &to)) { igraphmodule_handle_igraph_error(); return NULL; } - return PyInt_FromLong((long int)from); + return PyLong_FromLong((long int)from); } /** @@ -482,7 +464,7 @@ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) if (igraph_edge(&o->g, self->idx, &from, &to)) { igraphmodule_handle_igraph_error(); return NULL; } - return PyInt_FromLong((long)to); + return PyLong_FromLong((long)to); } /** @@ -508,7 +490,7 @@ PyObject* igraphmodule_Edge_get_target_vertex(igraphmodule_EdgeObject* self, voi * Returns the edge index */ PyObject* igraphmodule_Edge_get_index(igraphmodule_EdgeObject* self, void* closure) { - return PyInt_FromLong((long int)self->idx); + return PyLong_FromLong((long int)self->idx); } /** diff --git a/src/_igraph/edgeobject.h b/src/_igraph/edgeobject.h index 837e145e1..2d7f3a259 100644 --- a/src/_igraph/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -25,7 +25,6 @@ #include #include "graphobject.h" -#include "py2compat.h" /** * \ingroup python_interface_edge @@ -35,7 +34,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; igraph_integer_t idx; - Py_hash_t hash; + long hash; } igraphmodule_EdgeObject; int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self); diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index fc7af6629..311df20dd 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -28,7 +28,6 @@ #include "edgeseqobject.h" #include "edgeobject.h" #include "error.h" -#include "py2compat.h" #include "pyhelpers.h" #define GET_GRAPH(obj) (((igraphmodule_GraphObject*)obj->gref)->g) @@ -116,9 +115,9 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, if (esobj == Py_None) { /* If es is None, we are selecting all the edges */ igraph_es_all(&es, IGRAPH_EDGEORDER_ID); - } else if (PyInt_Check(esobj)) { + } else if (PyLong_Check(esobj)) { /* We selected a single edge */ - long int idx = PyInt_AsLong(esobj); + long int idx = PyLong_AsLong(esobj); if (idx < 0 || idx >= igraph_ecount(&((igraphmodule_GraphObject*)g)->g)) { PyErr_SetString(PyExc_ValueError, "edge index out of range"); return -1; @@ -372,7 +371,7 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject return -1; } - if (PyString_Check(values) || !PySequence_Check(values)) { + if (PyUnicode_Check(values) || !PySequence_Check(values)) { /* If values is a string or not a sequence, we construct a list with a * single element (the value itself) and then call ourselves again */ int result; @@ -542,10 +541,10 @@ PyObject* igraphmodule_EdgeSeq_find(igraphmodule_EdgeSeqObject *self, PyObject * Py_DECREF(call_result); Py_DECREF(edge); } - } else if (PyInt_Check(item)) { + } else if (PyLong_Check(item)) { /* Integers are interpreted as indices on the edge set and NOT on the * original, untouched edge sequence of the graph */ - return PySequence_GetItem((PyObject*)self, PyInt_AsLong(item)); + return PySequence_GetItem((PyObject*)self, PyLong_AsLong(item)); } PyErr_SetString(PyExc_IndexError, "no such edge"); @@ -622,7 +621,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject } igraph_vector_destroy(&v); - } else if (PyInt_Check(item)) { + } else if (PyLong_Check(item)) { /* Integers are treated specially: from now on, all remaining items * in the argument list must be integers and they will be used together * to restrict the edge set. Integers are interpreted as indices on the @@ -648,14 +647,14 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject for (; i= m || idx < 0) { PyErr_SetString(PyExc_ValueError, "edge index out of range"); igraph_vector_destroy(&v); diff --git a/src/_igraph/filehandle.c b/src/_igraph/filehandle.c index f085d6f0b..94458fa99 100644 --- a/src/_igraph/filehandle.c +++ b/src/_igraph/filehandle.c @@ -21,104 +21,9 @@ */ #include "filehandle.h" -#include "py2compat.h" #include "pyhelpers.h" #ifndef PYPY_VERSION -# ifndef IGRAPH_PYTHON3 -static int igraphmodule_i_filehandle_init_cpython_2(igraphmodule_filehandle_t* handle, - PyObject* object, char* mode) { - FILE* fp; - PyObject* fileno_method; - PyObject* fileno_result; - int fileno = -1; - - if (object == 0) { - PyErr_SetString(PyExc_TypeError, "trying to convert a null object " - "to a file handle"); - return 1; - } - - handle->object = 0; - handle->need_close = 0; - - if (PyBaseString_Check(object)) { - /* We have received a string; we need to open the file denoted by this - * string now and mark that we opened the file ourselves (so we need - * to close it when igraphmodule_filehandle_destroy is invoked). */ - handle->object = PyFile_FromString(PyString_AsString(object), mode); - if (handle->object == 0) { - /* Could not open the file; just return an error code because an - * exception was raised already */ - return 1; - } - /* Remember that we need to close the file ourselves */ - handle->need_close = 1; - /* Get a FILE* object from the file */ - fp = PyFile_AsFile(handle->object); - } else if (PyFile_Check(object)) { - /* This is a file-like object; store a reference for it and - * we will handle it later */ - handle->object = object; - Py_INCREF(handle->object); - /* Get a FILE* object from the file */ - fp = PyFile_AsFile(handle->object); - } else { - /* Check whether the object has a fileno() method. If so, we convert - * that to a file descriptor and then fdopen() it */ - fileno_method = PyObject_GetAttrString(object, "fileno"); - if (fileno_method != 0) { - if (PyCallable_Check(fileno_method)) { - fileno_result = PyObject_CallObject(fileno_method, 0); - Py_DECREF(fileno_method); - if (fileno_result != 0) { - if (PyInt_Check(fileno_result)) { - fileno = (int)PyInt_AsLong(fileno_result); - Py_DECREF(fileno_result); - } else { - Py_DECREF(fileno_result); - PyErr_SetString(PyExc_TypeError, - "fileno() method of file-like object should return " - "an integer"); - return 1; - } - } else { - /* Exception set already by PyObject_CallObject() */ - return 1; - } - } else { - Py_DECREF(fileno_method); - PyErr_SetString(PyExc_TypeError, - "fileno() attribute of file-like object must be callable"); - return 1; - } - } else { - PyErr_SetString(PyExc_TypeError, "expected filename or file-like object"); - return 1; - } - - if (fileno > 0) { - fp = fdopen(fileno, mode); - } else { - PyErr_SetString(PyExc_ValueError, "fileno() method returned invalid " - "file descriptor"); - return 1; - } - } - - handle->fp = fp; - if (handle->fp == 0) { - igraphmodule_filehandle_destroy(handle); - /* This already called Py_DECREF(handle->object), no need to call it */ - PyErr_SetString(PyExc_RuntimeError, "PyFile_AsFile() failed unexpectedly"); - return 1; - } - - return 0; -} - -# else /* IGRAPH_PYTHON3 */ - static int igraphmodule_i_filehandle_init_cpython_3(igraphmodule_filehandle_t* handle, PyObject* object, char* mode) { int fp; @@ -135,7 +40,7 @@ static int igraphmodule_i_filehandle_init_cpython_3(igraphmodule_filehandle_t* h /* We have received a string; we need to open the file denoted by this * string now and mark that we opened the file ourselves (so we need * to close it when igraphmodule_filehandle_destroy is invoked). */ - handle->object = PyFile_FromObject(object, mode); + handle->object = igraphmodule_PyFile_FromObject(object, mode); if (handle->object == 0) { /* Could not open the file; just return an error code because an * exception was raised already */ @@ -170,75 +75,7 @@ static int igraphmodule_i_filehandle_init_cpython_3(igraphmodule_filehandle_t* h return 0; } -# endif /* IGRAPH_PYTHON3 */ -#endif /* PYPY_VERSION */ - -#ifdef PYPY_VERSION -# ifndef IGRAPH_PYTHON3 -static int igraphmodule_i_filehandle_init_pypy_2(igraphmodule_filehandle_t* handle, - PyObject* object, char* mode) { - int fp; - PyObject* fpobj; - char* fname; - - if (object == 0) { - PyErr_SetString(PyExc_TypeError, "trying to convert a null object " - "to a file handle"); - return 1; - } - - handle->need_close = 0; - handle->object = 0; - - if (PyBaseString_Check(object)) { - /* We have received a string; we need to open the file denoted by this - * string now and mark that we opened the file ourselves (so we need - * to close it when igraphmodule_filehandle_destroy is invoked). */ - handle->object = PyFile_FromString(PyString_AsString(object), mode); - if (handle->object == 0) { - /* Could not open the file; just return an error code because an - * exception was raised already */ - return 1; - } - /* Remember that we need to close the file ourselves */ - handle->need_close = 1; - } else { - /* This is probably a file-like object; store a reference for it and - * we will handle it later */ - handle->object = object; - Py_INCREF(handle->object); - } - - /* PyPy does not have PyFile_AsFile, so we will try to access the file - * descriptor instead by calling its fileno() method and then opening the - * file handle with fdopen */ - fpobj = PyObject_CallMethod(handle->object, "fileno", 0); - if (fpobj == 0 || !PyInt_Check(fpobj)) { - if (fpobj != 0) { - Py_DECREF(fpobj); - } - igraphmodule_filehandle_destroy(handle); - /* This already called Py_DECREF(handle->object), no need to call it. - * Also, an exception was raised by PyObject_CallMethod so no need to - * raise one ourselves */ - return 1; - } - fp = (int)PyInt_AsLong(fpobj); - Py_DECREF(fpobj); - - handle->fp = fdopen(fp, mode); - if (handle->fp == 0) { - igraphmodule_filehandle_destroy(handle); - /* This already called Py_DECREF(handle->object), no need to call it */ - PyErr_SetString(PyExc_RuntimeError, "fdopen() failed unexpectedly"); - return 1; - } - - return 0; -} - -# else /* IGRAPH_PYTHON3 */ - +#else /* PYPY_VERSION */ static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* handle, PyObject* object, char* mode) { int fp; @@ -289,7 +126,6 @@ static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* hand return 0; } -# endif /* IGRAPH_PYTHON3 */ #endif /** @@ -302,17 +138,9 @@ static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* hand int igraphmodule_filehandle_init(igraphmodule_filehandle_t* handle, PyObject* object, char* mode) { #ifdef PYPY_VERSION -# ifdef IGRAPH_PYTHON3 return igraphmodule_i_filehandle_init_pypy_3(handle, object, mode); -# else - return igraphmodule_i_filehandle_init_pypy_2(handle, object, mode); -# endif #else -# ifdef IGRAPH_PYTHON3 return igraphmodule_i_filehandle_init_cpython_3(handle, object, mode); -# else - return igraphmodule_i_filehandle_init_cpython_2(handle, object, mode); -# endif #endif } @@ -333,11 +161,11 @@ void igraphmodule_filehandle_destroy(igraphmodule_filehandle_t* handle) { handle->fp = 0; if (handle->object != 0) { - /* PyFile_Close might mess up the stored exception, so let's + /* igraphmodule_PyFile_Close might mess up the stored exception, so let's * store the current exception state and restore it */ PyErr_Fetch(&exc_type, &exc_value, &exc_traceback); if (handle->need_close) { - if (PyFile_Close(handle->object)) { + if (igraphmodule_PyFile_Close(handle->object)) { PyErr_WriteUnraisable(Py_None); } } diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e979b394a..748114d1d 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -32,7 +32,6 @@ #include "graphobject.h" #include "indexing.h" #include "memory.h" -#include "py2compat.h" #include "pyhelpers.h" #include "vertexseqobject.h" #include @@ -347,11 +346,11 @@ PyObject* igraphmodule_Graph_from_igraph_t(igraph_t *graph) { PyObject *igraphmodule_Graph_str(igraphmodule_GraphObject * self) { if (igraph_is_directed(&self->g)) - return PyString_FromFormat("Directed graph (|V| = %ld, |E| = %ld)", + return PyUnicode_FromFormat("Directed graph (|V| = %ld, |E| = %ld)", (long)igraph_vcount(&self->g), (long)igraph_ecount(&self->g)); else - return PyString_FromFormat("Undirected graph (|V| = %ld, |E| = %ld)", + return PyUnicode_FromFormat("Undirected graph (|V| = %ld, |E| = %ld)", (long)igraph_vcount(&self->g), (long)igraph_ecount(&self->g)); } @@ -729,7 +728,7 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, if (!return_single) list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); else - list = PyInt_FromLong((long int)VECTOR(result)[0]); + list = PyLong_FromLong((long int)VECTOR(result)[0]); igraph_vector_destroy(&result); igraph_vs_destroy(&vs); @@ -953,7 +952,7 @@ PyObject *igraphmodule_Graph_maxdegree(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); - return PyInt_FromLong((long)result); + return PyLong_FromLong((long)result); } /** \ingroup python_interface_graph @@ -1155,7 +1154,7 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, if (!return_single) list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); else - list = PyInt_FromLong((long int)VECTOR(result)[0]); + list = PyLong_FromLong((long int)VECTOR(result)[0]); igraph_vector_destroy(&result); igraph_es_destroy(&es); @@ -1572,7 +1571,7 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, /* The diameter is integer in this case, except if igraph_diameter() * returned NaN or infinity for some reason */ if (ceilf(diameter) == diameter && isfinite(diameter)) { - return PyInt_FromLong((long)diameter); + return PyLong_FromLong((long)diameter); } else { return PyFloat_FromDouble((double)diameter); } @@ -1714,7 +1713,7 @@ PyObject *igraphmodule_Graph_girth(igraphmodule_GraphObject *self, igraph_vector_destroy(&vids); return o; } - return PyInt_FromLong((long)girth); + return PyLong_FromLong((long)girth); } /** @@ -2013,8 +2012,8 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, m = 1; } else if (m_obj != 0) { /* let's check whether we have a constant out-degree or a list */ - if (PyInt_Check(m_obj)) { - m = PyInt_AsLong(m_obj); + if (PyLong_Check(m_obj)) { + m = PyLong_AsLong(m_obj); igraph_vector_init(&outseq, 0); } else if (PyList_Check(m_obj)) { if (igraphmodule_PyObject_to_vector_t(m_obj, &outseq, 1)) { @@ -3084,8 +3083,8 @@ NULL }; } // let's check whether we have a constant out-degree or a list - if (PyInt_Check(m_obj)) { - m = PyInt_AsLong(m_obj); + if (PyLong_Check(m_obj)) { + m = PyLong_AsLong(m_obj); igraph_vector_init(&outseq, 0); } else if (PyList_Check(m_obj)) { @@ -3461,7 +3460,7 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, if (attr_o != Py_None) { s = PyObject_Str(attr_o); if (s) { - attr = PyString_CopyAsString(s); + attr = PyUnicode_CopyAsString(s); if (attr == 0) return NULL; } else return NULL; @@ -4744,13 +4743,13 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * igraph_vector_ptr_t *ptrvec=0; igraph_bool_t use_edges = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOOO!", kwlist, &from_o, - &to_o, &weights_o, &mode_o, &PyString_Type, &output_o)) + &to_o, &weights_o, &mode_o, &PyUnicode_Type, &output_o)) return NULL; if (output_o == 0 || output_o == Py_None || - PyString_IsEqualToASCIIString(output_o, "vpath")) { + PyUnicode_IsEqualToASCIIString(output_o, "vpath")) { use_edges = 0; - } else if (PyString_IsEqualToASCIIString(output_o, "epath")) { + } else if (PyUnicode_IsEqualToASCIIString(output_o, "epath")) { use_edges = 1; } else { PyErr_SetString(PyExc_ValueError, "output argument must be \"vpath\" or \"epath\""); @@ -5164,7 +5163,7 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, if (!return_single) result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); else - result = PyInt_FromLong((long)VECTOR(res)[0]); + result = PyLong_FromLong((long)VECTOR(res)[0]); igraph_vector_destroy(&res); @@ -6319,7 +6318,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, } igraph_vector_destroy(&cut_prob); - return PyInt_FromLong((long)result); + return PyLong_FromLong((long)result); } /** \ingroup python_interface_graph @@ -6356,9 +6355,9 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s } } - if (PyInt_Check(sample)) { + if (PyLong_Check(sample)) { /* samples chosen randomly */ - long int ns = PyInt_AsLong(sample); + long int ns = PyLong_AsLong(sample); if (igraph_motifs_randesu_estimate(&self->g, &result, (igraph_integer_t) size, &cut_prob, (igraph_integer_t) ns, 0)) { igraphmodule_handle_igraph_error(); @@ -6381,7 +6380,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s } igraph_vector_destroy(&cut_prob); - return PyInt_FromLong((long)result); + return PyLong_FromLong((long)result); } /** \ingroup python_interface_graph @@ -7458,7 +7457,7 @@ PyObject *igraphmodule_Graph_layout_bipartite( } if (types_o == Py_None) { - types_o = PyString_FromString("type"); + types_o = PyUnicode_FromString("type"); } else { Py_INCREF(types_o); } @@ -8097,7 +8096,7 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, return NULL; if (capacity_obj == Py_None) { - capacity_obj = PyString_FromString("capacity"); + capacity_obj = PyUnicode_FromString("capacity"); } else { Py_INCREF(capacity_obj); } @@ -8225,7 +8224,7 @@ PyObject *igraphmodule_Graph_write_gml(igraphmodule_GraphObject * self, igraphmodule_filehandle_destroy(&fobj); } - creator_str = PyString_CopyAsString(o); + creator_str = PyUnicode_CopyAsString(o); Py_DECREF(o); if (creator_str == 0) { @@ -8502,7 +8501,7 @@ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, } } - return PyInt_FromLong((long)isoclass); + return PyLong_FromLong((long)isoclass); } /** \ingroup python_interface_graph @@ -10927,7 +10926,7 @@ PyObject *igraphmodule_Graph_clique_number(igraphmodule_GraphObject * self) if (igraph_clique_number(&self->g, &i)) return igraphmodule_handle_igraph_error(); - result = PyInt_FromLong((long)i); + result = PyLong_FromLong((long)i); return result; } @@ -11088,7 +11087,7 @@ PyObject *igraphmodule_Graph_independence_number(igraphmodule_GraphObject * if (igraph_independence_number(&self->g, &i)) return igraphmodule_handle_igraph_error(); - result = PyInt_FromLong((long)i); + result = PyLong_FromLong((long)i); return result; } @@ -11923,25 +11922,12 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, /** \defgroup python_interface_internal Internal functions * \ingroup python_interface */ -#ifdef IGRAPH_PYTHON3 PyObject *igraphmodule_Graph___graph_as_capsule__(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { return PyCapsule_New((void *)&self->g, 0, 0); } -#else -/** \ingroup python_interface_internal - * \brief Returns the encapsulated igraph graph as a PyCObject - * \return a new PyCObject - */ -PyObject *igraphmodule_Graph___graph_as_cobject__(igraphmodule_GraphObject * - self, PyObject * args, - PyObject * kwds) -{ - return PyCObject_FromVoidPtr((void *)&self->g, 0); -} -#endif /** \ingroup python_interface_internal * \brief Returns the pointer of the encapsulated igraph graph as an ordinary @@ -11949,7 +11935,7 @@ PyObject *igraphmodule_Graph___graph_as_cobject__(igraphmodule_GraphObject * * module without any additional conversions. */ PyObject *igraphmodule_Graph__raw_pointer(igraphmodule_GraphObject *self) { - return PyInt_FromLong((long int)&self->g); + return PyLong_FromLong((long int)&self->g); } /** \ingroup python_interface_internal @@ -15975,7 +15961,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /**********************/ /* INTERNAL FUNCTIONS */ /**********************/ -#ifdef IGRAPH_PYTHON3 {"__graph_as_capsule", (PyCFunction) igraphmodule_Graph___graph_as_capsule__, METH_VARARGS | METH_KEYWORDS, @@ -15986,18 +15971,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Python object. This function should not be used directly by igraph\n" "users, it is useful only in the case when the underlying igraph object\n" "must be passed to other C code through Python.\n\n"}, -#else - {"__graph_as_cobject", - (PyCFunction) igraphmodule_Graph___graph_as_cobject__, - METH_VARARGS | METH_KEYWORDS, - "__graph_as_cobject()\n--\n\n" - "Returns the igraph graph encapsulated by the Python object as\n" - "a PyCObject\n\n." - "A PyCObject is practically a regular C pointer, wrapped in a\n" - "Python object. This function should not be used directly by igraph\n" - "users, it is useful only in the case when the underlying igraph object\n" - "must be passed to other C code through Python.\n\n"}, -#endif {"_raw_pointer", (PyCFunction) igraphmodule_Graph__raw_pointer, @@ -16040,9 +16013,6 @@ PyNumberMethods igraphmodule_Graph_as_number = { 0, /* nb_add */ 0, /*nb_subtract */ 0, /*nb_multiply */ -#ifndef IGRAPH_PYTHON3 - 0, /*nb_divide */ -#endif 0, /*nb_remainder */ 0, /*nb_divmod */ 0, /*nb_power */ @@ -16056,22 +16026,12 @@ PyNumberMethods igraphmodule_Graph_as_number = { 0, /*nb_and */ 0, /*nb_xor */ 0, /*nb_or */ -#ifndef IGRAPH_PYTHON3 - 0, /*nb_coerce */ -#endif 0, /*nb_int */ 0, /*nb_long (2.x) / nb_reserved (3.x)*/ 0, /*nb_float */ -#ifndef IGRAPH_PYTHON3 - 0, /*nb_oct */ - 0, /*nb_hex */ -#endif 0, /*nb_inplace_add */ 0, /*nb_inplace_subtract */ 0, /*nb_inplace_multiply */ -#ifndef IGRAPH_PYTHON3 - 0, /*nb_inplace_divide */ -#endif 0, /*nb_inplace_remainder */ 0, /*nb_inplace_power */ 0, /*nb_inplace_lshift */ @@ -16079,14 +16039,11 @@ PyNumberMethods igraphmodule_Graph_as_number = { 0, /*nb_inplace_and */ 0, /*nb_inplace_xor */ 0, /*nb_inplace_or */ - -#ifdef IGRAPH_PYTHON3 0, /*nb_floor_divide */ 0, /*nb_true_divide */ 0, /*nb_inplace_floor_divide */ 0, /*nb_inplace_true_divide */ 0, /*nb_index */ -#endif }; /** \ingroup python_interface_graph diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 51091ccc5..856d4bf7a 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -33,7 +33,6 @@ #include "edgeseqobject.h" #include "error.h" #include "graphobject.h" -#include "py2compat.h" #include "random.h" #include "vertexobject.h" #include "vertexseqobject.h" @@ -130,7 +129,6 @@ static struct module_state _state = { 0, 0 }; #define GETSTATE(m) (&_state) -#ifdef IGRAPH_PYTHON3 static int igraphmodule_traverse(PyObject *m, visitproc visit, void* arg) { Py_VISIT(GETSTATE(m)->progress_handler); Py_VISIT(GETSTATE(m)->status_handler); @@ -142,7 +140,6 @@ static int igraphmodule_clear(PyObject *m) { Py_CLEAR(GETSTATE(m)->status_handler); return 0; } -#endif static int igraphmodule_igraph_interrupt_hook(void* data) { if (PyErr_CheckSignals()) { @@ -735,9 +732,8 @@ static PyMethodDef igraphmodule_methods[] = "Should not be used directly.\n" /** - * Module definition table (only for Python 3.x) + * Module definition table */ -#ifdef IGRAPH_PYTHON3 static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "igraph._igraph", /* m_name */ @@ -749,7 +745,6 @@ static struct PyModuleDef moduledef = { igraphmodule_clear, /* m_clear */ 0 /* m_free */ }; -#endif /****************** Exported API functions *******************/ @@ -796,16 +791,8 @@ igraph_t* PyIGraph_ToCGraph(PyObject* graph) { extern PyObject* igraphmodule_InternalError; extern PyObject* igraphmodule_arpack_options_default; -#ifdef IGRAPH_PYTHON3 -# define INITERROR return NULL - PyObject* PyInit__igraph(void) -#else -# define INITERROR return -# ifndef PyMODINIT_FUNC -# define PyMODINIT_FUNC void -# endif - PyMODINIT_FUNC init_igraph(void) -#endif +#define INITERROR return NULL +PyObject* PyInit__igraph(void) { PyObject* m; static void *PyIGraph_API[PyIGraph_API_pointers]; @@ -845,12 +832,7 @@ extern PyObject* igraphmodule_arpack_options_default; INITERROR; /* Initialize the core module */ -#ifdef IGRAPH_PYTHON3 m = PyModule_Create(&moduledef); -#else - m = Py_InitModule3("igraph._igraph", igraphmodule_methods, MODULE_DOCS); -#endif - if (m == NULL) INITERROR; @@ -945,18 +927,12 @@ extern PyObject* igraphmodule_arpack_options_default; PyIGraph_API[PyIGraph_ToCGraph_NUM] = (void *)PyIGraph_ToCGraph; /* Create a CObject containing the API pointer array's address */ -#ifdef IGRAPH_PYTHON3 c_api_object = PyCapsule_New((void*)PyIGraph_API, "igraph._igraph._C_API", 0); -#else - c_api_object = PyCObject_FromVoidPtr((void*)PyIGraph_API, 0); -#endif if (c_api_object != 0) { PyModule_AddObject(m, "_C_API", c_api_object); } igraphmodule_initialized = 1; -#ifdef IGRAPH_PYTHON3 return m; -#endif } diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index f30729b17..8eececb9c 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -26,7 +26,6 @@ #include "error.h" #include "indexing.h" #include "platform.h" -#include "py2compat.h" #include "pyhelpers.h" /***************************************************************************/ @@ -41,7 +40,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pa if (eid >= 0) { /* Edge found, get the value of the attribute */ if (values == 0) { - return PyInt_FromLong(1L); + return PyLong_FromLong(1L); } else { result = PyList_GetItem(values, eid); Py_XINCREF(result); @@ -49,7 +48,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_indexing_get_value_for_vertex_pa } } else { /* No such edge, return zero */ - return PyInt_FromLong(0L); + return PyLong_FromLong(0L); } } @@ -160,7 +159,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, if (values) item = PyList_GetItem(values, eid); else - item = PyInt_FromLong(1); + item = PyLong_FromLong(1); Py_INCREF(item); PyList_SetItem(result, v, item); /* reference stolen here */ } @@ -221,7 +220,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, */ static INLINE igraph_bool_t deleting_edge(PyObject* value) { return value == Py_None || value == Py_False || - (PyInt_Check(value) && PyInt_AsLong(value) == 0); + (PyLong_Check(value) && PyLong_AsLong(value) == 0); } /** diff --git a/src/_igraph/py2compat.c b/src/_igraph/py2compat.c deleted file mode 100644 index 31fbec867..000000000 --- a/src/_igraph/py2compat.c +++ /dev/null @@ -1,146 +0,0 @@ -/* -*- mode: C -*- */ -/* vim: set ts=2 sw=2 sts=2 et: */ - -/* - IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - - 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 2 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, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - -*/ - -#include "py2compat.h" - -/* Common utility functions that are useful both in Python 2.x and 3.x */ - -int PyFile_Close(PyObject* fileObj) { - PyObject *result; - - result = PyObject_CallMethod(fileObj, "close", 0); - if (result) { - Py_DECREF(result); - return 0; - } else { - /* Exception raised already */ - return 1; - } -} - - -#ifdef IGRAPH_PYTHON3 - -/* Python 3.x functions */ - -PyObject* PyFile_FromObject(PyObject* filename, const char* mode) { - PyObject *ioModule, *fileObj; - - ioModule = PyImport_ImportModule("io"); - if (ioModule == 0) - return 0; - - fileObj = PyObject_CallMethod(ioModule, "open", "Os", filename, mode); - Py_DECREF(ioModule); - - return fileObj; -} - -char* PyString_CopyAsString(PyObject* string) { - PyObject* bytes; - char* result; - - if (PyBytes_Check(string)) { - bytes = string; - Py_INCREF(bytes); - } else { - bytes = PyUnicode_AsUTF8String(string); - } - - if (bytes == 0) - return 0; - - result = strdup(PyBytes_AS_STRING(bytes)); - Py_DECREF(bytes); - - if (result == 0) - PyErr_NoMemory(); - - return result; -} - -int PyString_IsEqualToUTF8String(PyObject* py_string, - const char* c_string) { - PyObject* c_string_conv; - int result; - - if (!PyUnicode_Check(py_string)) - return 0; - - c_string_conv = PyUnicode_FromString(c_string); - if (c_string_conv == 0) - return 0; - - result = (PyUnicode_Compare(py_string, c_string_conv) == 0); - Py_DECREF(c_string_conv); - - return result; -} - -#else - -/* Python 2.x functions */ - -char* PyString_CopyAsString(PyObject* string) { - char* result; - - if (!PyBaseString_Check(string)) { - PyErr_SetString(PyExc_TypeError, "string or unicode object expected"); - return 0; - } - - result = PyString_AsString(string); - if (result == 0) - return 0; - - result = strdup(result); - if (result == 0) - PyErr_NoMemory(); - - return result; -} - -int PyString_IsEqualToASCIIString(PyObject* py_string, - const char* c_string) { - PyObject* c_string_conv; - int result; - - if (PyString_Check(py_string)) { - return strcmp(PyString_AS_STRING(py_string), c_string) == 0; - } - - if (!PyUnicode_Check(py_string)) - return 0; - - c_string_conv = PyUnicode_DecodeASCII(c_string, strlen(c_string), "strict"); - if (c_string_conv == 0) - return 0; - - result = (PyUnicode_Compare(py_string, c_string_conv) == 0); - Py_DECREF(c_string_conv); - - return result; -} - -#endif diff --git a/src/_igraph/py2compat.h b/src/_igraph/py2compat.h deleted file mode 100644 index 8ab6dd56f..000000000 --- a/src/_igraph/py2compat.h +++ /dev/null @@ -1,94 +0,0 @@ -/* -*- mode: C -*- */ -/* vim: set ts=2 sw=2 sts=2 et: */ - -/* - IGraph library. - Copyright (C) 2006-2012 Tamas Nepusz - - 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 2 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, write to the Free Software - Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA - 02110-1301 USA - -*/ - -#ifndef PY_IGRAPH_PY2COMPAT_H -#define PY_IGRAPH_PY2COMPAT_H - -#include - -/* Common utility functions */ -int PyFile_Close(PyObject* fileObj); - -/* Compatibility hacks */ -#ifndef Py_hash_t -# define Py_hash_t long -#endif - -#if PY_MAJOR_VERSION >= 3 - -/* Python 3.x-specific part follows here */ -#define IGRAPH_PYTHON3 - -#define PyBaseString_Check(o) (PyUnicode_Check(o) || PyBytes_Check(o)) - -PyObject* PyFile_FromObject(PyObject* filename, const char* mode); - -#ifndef PYPY_VERSION - typedef PyLongObject PyIntObject; -#endif /* PYPY_VERSION */ -#define PyInt_AsLong PyLong_AsLong -#define PyInt_Check PyLong_Check -#define PyInt_FromLong PyLong_FromLong - -#define PyNumber_Int PyNumber_Long - -#define PyString_AS_STRING PyUnicode_AS_UNICODE -#define PyString_Check PyUnicode_Check -#define PyString_FromFormat PyUnicode_FromFormat -#define PyString_FromString PyUnicode_FromString -#define PyString_Type PyUnicode_Type -#define PyString_IsEqualToASCIIString(uni, string) \ - (PyUnicode_CompareWithASCIIString(uni, string) == 0) - -#ifndef PyVarObject_HEAD_INIT -#define PyVarObject_HEAD_INIT(type, size) \ - PyObject_HEAD_INIT(type) size, -#endif - -int PyString_IsEqualToUTF8String(PyObject* py_string, - const char* c_string); - -#else - -/* Python 2.x-specific part follows here */ - -#define PyBaseString_Check(o) (PyString_Check(o) || PyUnicode_Check(o)) - -int PyString_IsEqualToASCIIString(PyObject* py_string, - const char* c_string); - -#ifndef Py_TYPE -# define Py_TYPE(o) ((o)->ob_type) -#endif - -#ifndef PyVarObject_HEAD_INIT -# define PyVarObject_HEAD_INIT(type, size) PyObject_HEAD_INIT(type) size, -#endif - -#endif - -char* PyString_CopyAsString(PyObject* string); - -#endif - diff --git a/src/_igraph/pyhelpers.c b/src/_igraph/pyhelpers.c index 633e4d73b..a51b60ca7 100644 --- a/src/_igraph/pyhelpers.c +++ b/src/_igraph/pyhelpers.c @@ -21,9 +21,41 @@ */ -#include "py2compat.h" #include "pyhelpers.h" +/** + * Closes a Python file-like object by calling its close() method. + */ +int igraphmodule_PyFile_Close(PyObject* fileObj) { + PyObject *result; + + result = PyObject_CallMethod(fileObj, "close", 0); + if (result) { + Py_DECREF(result); + return 0; + } else { + /* Exception raised already */ + return 1; + } +} + +/** + * Creates a Python file-like object from a Python object storing a string and + * an ordinary C string storing the mode to open the file in. + */ +PyObject* igraphmodule_PyFile_FromObject(PyObject* filename, const char* mode) { + PyObject *ioModule, *fileObj; + + ioModule = PyImport_ImportModule("io"); + if (ioModule == 0) + return 0; + + fileObj = PyObject_CallMethod(ioModule, "open", "Os", filename, mode); + Py_DECREF(ioModule); + + return fileObj; +} + /** * Creates a Python list and fills it with a pre-defined item. * @@ -51,7 +83,7 @@ PyObject* igraphmodule_PyList_NewFill(Py_ssize_t len, PyObject* item) { * \param len the length of the list to be created */ PyObject* igraphmodule_PyList_Zeroes(Py_ssize_t len) { - PyObject* zero = PyInt_FromLong(0); + PyObject* zero = PyLong_FromLong(0); PyObject* result; if (zero == 0) @@ -82,7 +114,7 @@ char* igraphmodule_PyObject_ConvertToCString(PyObject* string) { Py_INCREF(string); } - result = PyString_CopyAsString(string); + result = PyUnicode_CopyAsString(string); Py_DECREF(string); return result; @@ -101,22 +133,14 @@ PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssiz PyObject* result; if (builtin_module == 0) { -#ifdef IGRAPH_PYTHON3 builtin_module = PyImport_ImportModule("builtins"); -#else - builtin_module = PyImport_ImportModule("__builtin__"); -#endif if (builtin_module == 0) { return 0; } } if (range_func == 0) { -#ifdef IGRAPH_PYTHON3 range_func = PyObject_GetAttrString(builtin_module, "range"); -#else - range_func = PyObject_GetAttrString(builtin_module, "xrange"); -#endif if (range_func == 0) { return 0; } @@ -126,6 +150,47 @@ PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssiz return result; } +char* PyUnicode_CopyAsString(PyObject* string) { + PyObject* bytes; + char* result; + + if (PyBytes_Check(string)) { + bytes = string; + Py_INCREF(bytes); + } else { + bytes = PyUnicode_AsUTF8String(string); + } + + if (bytes == 0) + return 0; + + result = strdup(PyBytes_AS_STRING(bytes)); + Py_DECREF(bytes); + + if (result == 0) + PyErr_NoMemory(); + + return result; +} + +int PyUnicode_IsEqualToUTF8String(PyObject* py_string, + const char* c_string) { + PyObject* c_string_conv; + int result; + + if (!PyUnicode_Check(py_string)) + return 0; + + c_string_conv = PyUnicode_FromString(c_string); + if (c_string_conv == 0) + return 0; + + result = (PyUnicode_Compare(py_string, c_string_conv) == 0); + Py_DECREF(c_string_conv); + + return result; +} + /** * Generates a hash value for a plain C pointer. * diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h index 00c518e95..00bff1d86 100644 --- a/src/_igraph/pyhelpers.h +++ b/src/_igraph/pyhelpers.h @@ -26,12 +26,20 @@ #include +int igraphmodule_PyFile_Close(PyObject* fileObj); +PyObject* igraphmodule_PyFile_FromObject(PyObject* filename, const char* mode); PyObject* igraphmodule_PyList_NewFill(Py_ssize_t len, PyObject* item); PyObject* igraphmodule_PyList_Zeroes(Py_ssize_t len); char* igraphmodule_PyObject_ConvertToCString(PyObject* string); PyObject* igraphmodule_PyRange_create(Py_ssize_t start, Py_ssize_t stop, Py_ssize_t step); +int PyUnicode_IsEqualToUTF8String(PyObject* py_string, const char* c_string); long igraphmodule_Py_HashPointer(void *p); +#define PyBaseString_Check(o) (PyUnicode_Check(o) || PyBytes_Check(o)) +#define PyUnicode_IsEqualToASCIIString(uni, string) \ + (PyUnicode_CompareWithASCIIString(uni, string) == 0) +char* PyUnicode_CopyAsString(PyObject* string); + #define PY_IGRAPH_DEPRECATED(msg) \ PyErr_WarnEx(PyExc_DeprecationWarning, (msg), 1) #define PY_IGRAPH_WARN(msg) \ diff --git a/src/_igraph/random.c b/src/_igraph/random.c index bb623dfcc..e267d13d0 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -21,7 +21,6 @@ */ -#include "py2compat.h" #include "random.h" #include #include @@ -115,7 +114,7 @@ unsigned long int igraph_rng_Python_get(void *state) { /* Fallback to the C random generator */ return rand() * LONG_MAX; } - retval = PyInt_AsLong(result); + retval = PyLong_AsLong(result); Py_DECREF(result); return retval; } diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 138a225b1..f64b2c608 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -146,48 +146,31 @@ void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self) { PyObject *s; PyObject *attrs; -#ifndef IGRAPH_PYTHON3 - PyObject *grepr, *drepr; -#endif attrs = igraphmodule_Vertex_attributes(self); if (attrs == 0) return NULL; -#ifdef IGRAPH_PYTHON3 s = PyUnicode_FromFormat("igraph.Vertex(%R, %ld, %R)", (PyObject*)self->gref, (long int)self->idx, attrs); Py_DECREF(attrs); -#else - grepr=PyObject_Repr((PyObject*)self->gref); - drepr=PyObject_Repr(igraphmodule_Vertex_attributes(self)); - Py_DECREF(attrs); - if (!grepr || !drepr) { - Py_XDECREF(grepr); - Py_XDECREF(drepr); - return NULL; - } - s=PyString_FromFormat("igraph.Vertex(%s,%ld,%s)", PyString_AsString(grepr), - (long int)self->idx, PyString_AsString(drepr)); - Py_DECREF(grepr); - Py_DECREF(drepr); -#endif + return s; } /** \ingroup python_interface_vertex * \brief Returns the hash code of the vertex */ -Py_hash_t igraphmodule_Vertex_hash(igraphmodule_VertexObject* self) { - Py_hash_t hash_graph; - Py_hash_t hash_index; - Py_hash_t result; +long igraphmodule_Vertex_hash(igraphmodule_VertexObject* self) { + long hash_graph; + long hash_index; + long result; PyObject* index_o; if (self->hash != -1) return self->hash; - index_o = PyInt_FromLong((long int)self->idx); + index_o = PyLong_FromLong((long int)self->idx); if (index_o == 0) return -1; @@ -505,7 +488,7 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* if (!igraphmodule_attribute_name_check(k)) return -1; - if (PyString_IsEqualToASCIIString(k, "name")) + if (PyUnicode_IsEqualToASCIIString(k, "name")) igraphmodule_invalidate_vertex_name_index(&o->g); if (v==NULL) @@ -567,7 +550,7 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* * Returns the vertex index */ PyObject* igraphmodule_Vertex_get_index(igraphmodule_VertexObject* self, void* closure) { - return PyInt_FromLong((long int)self->idx); + return PyLong_FromLong((long int)self->idx); } /** @@ -628,7 +611,7 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje PyObject* v; int idx_int; - if (!PyInt_Check(idx)) { + if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_edge_list expected list of integers"); return NULL; } @@ -660,7 +643,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb PyObject* v; int idx_int; - if (!PyInt_Check(idx)) { + if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_vertex_list expected list of integers"); return NULL; } diff --git a/src/_igraph/vertexobject.h b/src/_igraph/vertexobject.h index b50b52351..f33d640eb 100644 --- a/src/_igraph/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -25,7 +25,6 @@ #include #include "graphobject.h" -#include "py2compat.h" /** * \ingroup python_interface_vertex @@ -36,7 +35,7 @@ typedef struct PyObject_HEAD igraphmodule_GraphObject* gref; igraph_integer_t idx; - Py_hash_t hash; + long hash; } igraphmodule_VertexObject; int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self); diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 88e476e7d..6560368cb 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -27,7 +27,6 @@ #include "common.h" #include "convert.h" #include "error.h" -#include "py2compat.h" #include "pyhelpers.h" #include "vertexseqobject.h" #include "vertexobject.h" @@ -115,9 +114,9 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, if (vsobj == Py_None) { /* If vs is None, we are selecting all the vertices */ igraph_vs_all(&vs); - } else if (PyInt_Check(vsobj)) { + } else if (PyLong_Check(vsobj)) { /* We selected a single vertex */ - long int idx = PyInt_AsLong(vsobj); + long int idx = PyLong_AsLong(vsobj); if (idx < 0 || idx >= igraph_vcount(&((igraphmodule_GraphObject*)g)->g)) { PyErr_SetString(PyExc_ValueError, "vertex index out of range"); return -1; @@ -353,7 +352,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb if (!igraphmodule_attribute_name_check(attrname)) return -1; - if (PyString_IsEqualToASCIIString(attrname, "name")) + if (PyUnicode_IsEqualToASCIIString(attrname, "name")) igraphmodule_invalidate_vertex_name_index(&gr->g); if (values == 0) { @@ -363,7 +362,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb return -1; } - if (PyString_Check(values) || !PySequence_Check(values)) { + if (PyUnicode_Check(values) || !PySequence_Check(values)) { /* If values is a string or not a sequence, we construct a list with a * single element (the value itself) and then call ourselves again */ int result; @@ -541,10 +540,10 @@ PyObject* igraphmodule_VertexSeq_find(igraphmodule_VertexSeqObject *self, PyObje Py_DECREF(call_result); Py_DECREF(vertex); } - } else if (PyInt_Check(item)) { + } else if (PyLong_Check(item)) { /* Integers are interpreted as indices on the vertex set and NOT on the * original, untouched vertex sequence of the graph */ - return PySequence_GetItem((PyObject*)self, PyInt_AsLong(item)); + return PySequence_GetItem((PyObject*)self, PyLong_AsLong(item)); } else if (PyBaseString_Check(item)) { /* Strings are interpreted as vertex names */ if (igraphmodule_get_vertex_id_by_name(&self->gref->g, item, &i)) @@ -651,7 +650,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, } igraph_vector_destroy(&v); - } else if (PyInt_Check(item)) { + } else if (PyLong_Check(item)) { /* Integers are treated specially: from now on, all remaining items * in the argument list must be integers and they will be used together * to restrict the vertex set. Integers are interpreted as indices on the @@ -677,14 +676,14 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, for (; i= m || idx < 0) { PyErr_SetString(PyExc_ValueError, "vertex index out of range"); igraph_vector_destroy(&v); From 119a706d9e2df1e6d302fb9d6826087effdf47fc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 7 Mar 2021 02:11:25 +0100 Subject: [PATCH 0196/1681] fix: fix compilation on PyPy 3.7 that I broke in the previous commit --- src/_igraph/filehandle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/filehandle.c b/src/_igraph/filehandle.c index 94458fa99..f7bc72f38 100644 --- a/src/_igraph/filehandle.c +++ b/src/_igraph/filehandle.c @@ -91,7 +91,7 @@ static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* hand /* We have received a string; we need to open the file denoted by this * string now and mark that we opened the file ourselves (so we need * to close it when igraphmodule_filehandle_destroy is invoked). */ - handle->object = PyFile_FromObject(object, mode); + handle->object = igraphmodule_PyFile_FromObject(object, mode); if (handle->object == 0) { /* Could not open the file; just return an error code because an * exception was raised already */ From 3dc0ec00f67d8cbcee31077229d6f33a514a3387 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 7 Mar 2021 02:26:41 +0100 Subject: [PATCH 0197/1681] doc: remove type annotations from __text_signature__ markup, it is not supported --- src/_igraph/arpackobject.c | 2 +- src/_igraph/edgeseqobject.c | 12 +++---- src/_igraph/graphobject.c | 62 +++++++++++++++++------------------ src/_igraph/vertexobject.c | 6 ++-- src/_igraph/vertexseqobject.c | 12 +++---- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 918a2f697..7f31f506c 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -190,7 +190,7 @@ PyObject* igraphmodule_ARPACKOptions_str( PyMethodDef igraphmodule_ARPACKOptions_methods[] = { /*{"attributes", (PyCFunction)igraphmodule_Edge_attributes, METH_NOARGS, - "attributes() -> list\n\n" + "attributes()\n--\n\n" "Returns the attribute list of the graph's edges\n" },*/ {NULL} diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 311df20dd..507edd3ec 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -796,36 +796,36 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject PyMethodDef igraphmodule_EdgeSeq_methods[] = { {"attribute_names", (PyCFunction)igraphmodule_EdgeSeq_attribute_names, METH_NOARGS, - "attribute_names() -> list\n--\n\n" + "attribute_names()\n--\n\n" "Returns the attribute name list of the graph's edges\n" }, {"find", (PyCFunction)igraphmodule_EdgeSeq_find, METH_VARARGS, - "find(condition) -> Edge\n--\n\n" + "find(condition)\n--\n\n" "For internal use only.\n" }, {"get_attribute_values", (PyCFunction)igraphmodule_EdgeSeq_get_attribute_values, METH_O, - "get_attribute_values(attrname) -> list\n--\n\n" + "get_attribute_values(attrname)\n--\n\n" "Returns the value of a given edge attribute for all edges.\n\n" "@param attrname: the name of the attribute\n" }, {"is_all", (PyCFunction)igraphmodule_EdgeSeq_is_all, METH_NOARGS, - "is_all() -> bool\n--\n\n" + "is_all()\n--\n\n" "Returns whether the edge sequence contains all the edges exactly once, in\n" "the order of their edge IDs.\n\n" "This is used for optimizations in some of the edge selector routines.\n" }, {"set_attribute_values", (PyCFunction)igraphmodule_EdgeSeq_set_attribute_values, METH_VARARGS | METH_KEYWORDS, - "set_attribute_values(attrname, values) -> list\n--\n\n" + "set_attribute_values(attrname, values)\n--\n\n" "Sets the value of a given edge attribute for all vertices\n" "@param attrname: the name of the attribute\n" "@param values: the new attribute values in a list\n" }, {"select", (PyCFunction)igraphmodule_EdgeSeq_select, METH_VARARGS, - "select(...) -> VertexSeq\n--\n\n" + "select(...)\n--\n\n" "For internal use only.\n" }, {NULL} diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 748114d1d..a488697b4 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -11984,21 +11984,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_vcount {"vcount", (PyCFunction) igraphmodule_Graph_vcount, METH_NOARGS, - "vcount() -> int\n--\n\n" + "vcount()\n--\n\n" "Counts the number of vertices.\n" "@return: the number of vertices in the graph.\n" "@rtype: integer"}, // interface to igraph_ecount {"ecount", (PyCFunction) igraphmodule_Graph_ecount, METH_NOARGS, - "ecount() -> int\n--\n\n" + "ecount()\n--\n\n" "Counts the number of edges.\n" "@return: the number of edges in the graph.\n" "@rtype: integer"}, // interface to igraph_is_dag {"is_dag", (PyCFunction) igraphmodule_Graph_is_dag, METH_NOARGS, - "is_dag() -> bool\n--\n\n" + "is_dag()\n--\n\n" "Checks whether the graph is a DAG (directed acyclic graph).\n\n" "A DAG is a directed graph with no directed cycles.\n\n" "@return: C{True} if it is a DAG, C{False} otherwise.\n" @@ -12007,7 +12007,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_is_directed {"is_directed", (PyCFunction) igraphmodule_Graph_is_directed, METH_NOARGS, - "is_directed() -> bool\n--\n\n" + "is_directed()\n--\n\n" "Checks whether the graph is directed.\n" "@return: C{True} if it is directed, C{False} otherwise.\n" "@rtype: boolean"}, @@ -12015,7 +12015,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_is_simple {"is_simple", (PyCFunction) igraphmodule_Graph_is_simple, METH_NOARGS, - "is_simple() -> bool\n--\n\n" + "is_simple()\n--\n\n" "Checks whether the graph is simple (no loop or multiple edges).\n\n" "@return: C{True} if it is simple, C{False} otherwise.\n" "@rtype: boolean"}, @@ -12023,14 +12023,14 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_add_vertices */ {"add_vertices", (PyCFunction) igraphmodule_Graph_add_vertices, METH_VARARGS, - "add_vertices(n: int) -> None\n--\n\n" + "add_vertices(n: int)\n--\n\n" "Adds vertices to the graph.\n\n" "@param n: the number of vertices to be added\n"}, /* interface to igraph_delete_vertices */ {"delete_vertices", (PyCFunction) igraphmodule_Graph_delete_vertices, METH_VARARGS, - "delete_vertices(vs) -> None\n--\n\n" + "delete_vertices(vs)\n--\n\n" "Deletes vertices and all its edges from the graph.\n\n" "@param vs: a single vertex ID or the list of vertex IDs\n" " to be deleted. No argument deletes all vertices.\n"}, @@ -12038,7 +12038,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_add_edges */ {"add_edges", (PyCFunction) igraphmodule_Graph_add_edges, METH_VARARGS, - "add_edges(es) -> None\n--\n\n" + "add_edges(es)\n--\n\n" "Adds edges to the graph.\n\n" "@param es: the list of edges to be added. Every edge is\n" " represented with a tuple, containing the vertex IDs of the\n" @@ -12047,7 +12047,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_delete_edges */ {"delete_edges", (PyCFunction) igraphmodule_Graph_delete_edges, METH_VARARGS | METH_KEYWORDS, - "delete_edges(es) -> None\n--\n\n" + "delete_edges(es)\n--\n\n" "Removes edges from the graph.\n\n" "All vertices will be kept, even if they lose all their edges.\n" "Nonexistent edges will be silently ignored.\n\n" @@ -12094,7 +12094,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_loop */ {"is_loop", (PyCFunction) igraphmodule_Graph_is_loop, METH_VARARGS | METH_KEYWORDS, - "is_loop(edges=None) -> List[bool]\n--\n\n" + "is_loop(edges=None)\n--\n\n" "Checks whether a specific set of edges contain loop edges\n\n" "@param edges: edge indices which we want to check. If C{None}, all\n" " edges are checked.\n" @@ -12103,7 +12103,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_multiple */ {"is_multiple", (PyCFunction) igraphmodule_Graph_is_multiple, METH_VARARGS | METH_KEYWORDS, - "is_multiple(edges=None) -> List[bool]\n--\n\n" + "is_multiple(edges=None)\n--\n\n" "Checks whether an edge is a multiple edge.\n\n" "Also works for a set of edges -- in this case, every edge is checked\n" "one by one. Note that if there are multiple edges going between a\n" @@ -12118,7 +12118,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_has_multiple */ {"has_multiple", (PyCFunction) igraphmodule_Graph_has_multiple, METH_NOARGS, - "has_multiple() -> bool\n--\n\n" + "has_multiple()\n--\n\n" "Checks whether the graph has multiple edges.\n\n" "@return: C{True} if the graph has at least one multiple edge,\n" " C{False} otherwise.\n" @@ -12127,7 +12127,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_mutual */ {"is_mutual", (PyCFunction) igraphmodule_Graph_is_mutual, METH_VARARGS | METH_KEYWORDS, - "is_mutual(edges=None) -> Lis[bool]\n--\n\n" + "is_mutual(edges=None)\n--\n\n" "Checks whether an edge has an opposite pair.\n\n" "Also works for a set of edges -- in this case, every edge is checked\n" "one by one. The result will be a list of booleans (or a single boolean\n" @@ -12146,7 +12146,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_count_multiple */ {"count_multiple", (PyCFunction) igraphmodule_Graph_count_multiple, METH_VARARGS | METH_KEYWORDS, - "count_multiple(edges=None) -> List[int]\n--\n\n" + "count_multiple(edges=None)\n--\n\n" "Counts the multiplicities of the given edges.\n\n" "@param edges: edge indices for which we want to count their\n" " multiplicity. If C{None}, all edges are counted.\n" @@ -12177,7 +12177,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_get_eid */ {"get_eid", (PyCFunction) igraphmodule_Graph_get_eid, METH_VARARGS | METH_KEYWORDS, - "get_eid(v1, v2, directed=True, error=True) -> int\n--\n\n" + "get_eid(v1, v2, directed=True, error=True)\n--\n\n" "Returns the edge ID of an arbitrary edge between vertices v1 and v2\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" @@ -12192,7 +12192,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_get_eids */ {"get_eids", (PyCFunction) igraphmodule_Graph_get_eids, METH_VARARGS | METH_KEYWORDS, - "get_eids(pairs=None, path=None, directed=True, error=True) -> List[int]\n--\n\n" + "get_eids(pairs=None, path=None, directed=True, error=True)\n--\n\n" "Returns the edge IDs of some edges between some vertices.\n\n" "This method can operate in two different modes, depending on which\n" "of the keyword arguments C{pairs} and C{path} are given.\n\n" @@ -12779,7 +12779,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_are_connected {"are_connected", (PyCFunction) igraphmodule_Graph_are_connected, METH_VARARGS | METH_KEYWORDS, - "are_connected(v1, v2) -> bool\n--\n\n" + "are_connected(v1, v2)\n--\n\n" "Decides whether two given vertices are directly connected.\n\n" "@param v1: the ID or name of the first vertex\n" "@param v2: the ID or name of the second vertex\n" @@ -13088,7 +13088,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_density */ {"density", (PyCFunction) igraphmodule_Graph_density, METH_VARARGS | METH_KEYWORDS, - "density(loops=False) -> float\n--\n\n" + "density(loops=False)\n--\n\n" "Calculates the density of the graph.\n\n" "@param loops: whether to take loops into consideration. If C{True},\n" " the algorithm assumes that there might be some loops in the graph\n" @@ -13412,7 +13412,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_bipartite */ {"is_bipartite", (PyCFunction) igraphmodule_Graph_is_bipartite, METH_VARARGS | METH_KEYWORDS, - "is_bipartite(return_types=False) -> bool\n--\n\n" + "is_bipartite(return_types=False)\n--\n\n" "Decides whether the graph is bipartite or not.\n\n" "Vertices of a bipartite graph can be partitioned into two groups A\n" "and B in a way that all edges go between the two groups.\n\n" @@ -13450,7 +13450,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_connected */ {"is_connected", (PyCFunction) igraphmodule_Graph_is_connected, METH_VARARGS | METH_KEYWORDS, - "is_connected(mode=STRONG) -> bool\n--\n\n" + "is_connected(mode=STRONG)\n--\n\n" "Decides whether the graph is connected.\n\n" "@param mode: whether we should calculate strong or weak connectivity.\n" "@return: C{True} if the graph is connected, C{False} otherwise.\n"}, @@ -13640,7 +13640,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_reciprocity */ {"reciprocity", (PyCFunction) igraphmodule_Graph_reciprocity, METH_VARARGS | METH_KEYWORDS, - "reciprocity(ignore_loops=True, mode=\"default\") -> float\n--\n\n" + "reciprocity(ignore_loops=True, mode=\"default\")\n--\n\n" "Reciprocity defines the proportion of mutual connections in a\n" "directed graph. It is most commonly defined as the probability\n" "that the opposite counterpart of a directed edge is also included\n" @@ -14908,7 +14908,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the isomorphism class of the (sub)graph\n\n"}, {"isomorphic", (PyCFunction) igraphmodule_Graph_isomorphic, METH_VARARGS | METH_KEYWORDS, - "isomorphic(other) -> bool\n--\n\n" + "isomorphic(other)\n--\n\n" "Checks whether the graph is isomorphic to another graph.\n\n" "The algorithm being used is selected using a simple heuristic:\n\n" " - If one graph is directed and the other undirected, an exception\n" @@ -15265,15 +15265,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { //////////////////////// {"attributes", (PyCFunction) igraphmodule_Graph_attributes, METH_NOARGS, - "attributes() -> Sequence[str]\n--\n\n" + "attributes()\n--\n\n" "@return: the attribute name list of the graph\n"}, {"vertex_attributes", (PyCFunction) igraphmodule_Graph_vertex_attributes, METH_NOARGS, - "vertex_attributes() -> Sequence[str]\n--\n\n" + "vertex_attributes()\n--\n\n" "@return: the attribute name list of the graph's vertices\n"}, {"edge_attributes", (PyCFunction) igraphmodule_Graph_edge_attributes, METH_NOARGS, - "edge_attributes() -> Sequence[str]\n--\n\n" + "edge_attributes()\n--\n\n" "@return: the attribute name list of the graph's edges\n"}, /////////////// @@ -15544,7 +15544,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@see: L{largest_cliques()} for the largest cliques."}, {"clique_number", (PyCFunction) igraphmodule_Graph_clique_number, METH_NOARGS, - "clique_number() -> int\n--\n\n" + "clique_number()\n--\n\n" "Returns the clique number of the graph.\n\n" "The clique number of the graph is the size of the largest clique.\n\n" "@see: L{largest_cliques()} for the largest cliques."}, @@ -15589,7 +15589,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"independence_number", (PyCFunction) igraphmodule_Graph_independence_number, METH_NOARGS, - "independence_number() -> int\n--\n\n" + "independence_number()\n--\n\n" "Returns the independence number of the graph.\n\n" "The independence number of the graph is the size of the largest\n" "independent vertex set.\n\n" @@ -15601,7 +15601,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /*********************************/ {"modularity", (PyCFunction) igraphmodule_Graph_modularity, METH_VARARGS | METH_KEYWORDS, - "modularity(membership, weights=None, resolution=1, directed=True) -> float\n--\n\n" + "modularity(membership, weights=None, resolution=1, directed=True)\n--\n\n" "Calculates the modularity of the graph with respect to some vertex types.\n\n" "The modularity of a graph w.r.t. some division measures how good the\n" "division is, or how separated are the different vertex types from each\n" @@ -15639,7 +15639,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"coreness", (PyCFunction) igraphmodule_Graph_coreness, METH_VARARGS | METH_KEYWORDS, - "coreness(mode=ALL) -> Sequence[int]\n--\n\n" + "coreness(mode=ALL)\n--\n\n" "Finds the coreness (shell index) of the vertices of the network.\n\n" "The M{k}-core of a graph is a maximal subgraph in which each vertex\n" "has at least degree k. (Degree here means the degree in the\n" @@ -15975,7 +15975,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"_raw_pointer", (PyCFunction) igraphmodule_Graph__raw_pointer, METH_NOARGS, - "_raw_pointer() -> int\n--\n\n" + "_raw_pointer()\n--\n\n" "Returns the memory address of the igraph graph encapsulated by the Python\n" "object as an ordinary Python integer.\n\n" "This function should not be used directly by igraph users, it is useful\n" @@ -15985,7 +15985,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"__register_destructor", (PyCFunction) igraphmodule_Graph___register_destructor__, METH_VARARGS | METH_KEYWORDS, - "__register_destructor(destructor) -> None\n--\n\n" + "__register_destructor(destructor)\n--\n\n" "Registers a destructor to be called when the object is freed by\n" "Python. This function should not be used directly by igraph users."}, diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index f64b2c608..0e5f28b77 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -733,17 +733,17 @@ GRAPH_PROXY_METHOD_PP(successors, "successors", _convert_to_vertex_list); PyMethodDef igraphmodule_Vertex_methods[] = { {"attributes", (PyCFunction)igraphmodule_Vertex_attributes, METH_NOARGS, - "attributes() -> dict\n--\n\n" + "attributes()\n--\n\n" "Returns a dict of attribute names and values for the vertex\n" }, {"attribute_names", (PyCFunction)igraphmodule_Vertex_attribute_names, METH_NOARGS, - "attribute_names() -> list\n--\n\n" + "attribute_names()\n--\n\n" "Returns the list of vertex attribute names\n" }, {"update_attributes", (PyCFunction)igraphmodule_Vertex_update_attributes, METH_VARARGS | METH_KEYWORDS, - "update_attributes(E, **F) -> None\n--\n\n" + "update_attributes(E, **F)\n--\n\n" "Updates the attributes of the vertex from dict/iterable E and F.\n\n" "If E has a C{keys()} method, it does: C{for k in E: self[k] = E[k]}.\n" "If E lacks a C{keys()} method, it does: C{for (k, v) in E: self[k] = v}.\n" diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 6560368cb..2101b22d3 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -898,17 +898,17 @@ PyObject* igraphmodule_VertexSeq__reindex_names(igraphmodule_VertexSeqObject* se PyMethodDef igraphmodule_VertexSeq_methods[] = { {"attribute_names", (PyCFunction)igraphmodule_VertexSeq_attribute_names, METH_NOARGS, - "attribute_names() -> list\n--\n\n" + "attribute_names()\n--\n\n" "Returns the attribute name list of the graph's vertices\n" }, {"find", (PyCFunction)igraphmodule_VertexSeq_find, METH_VARARGS, - "find(condition) -> Vertex\n--\n\n" + "find(condition)\n--\n\n" "For internal use only.\n" }, {"get_attribute_values", (PyCFunction)igraphmodule_VertexSeq_get_attribute_values, METH_O, - "get_attribute_values(attrname) -> list\n--\n\n" + "get_attribute_values(attrname)\n--\n\n" "Returns the value of a given vertex attribute for all vertices in a list.\n\n" "The values stored in the list are exactly the same objects that are stored\n" "in the vertex attribute, meaning that in the case of mutable objects,\n" @@ -919,18 +919,18 @@ PyMethodDef igraphmodule_VertexSeq_methods[] = { }, {"set_attribute_values", (PyCFunction)igraphmodule_VertexSeq_set_attribute_values, METH_VARARGS | METH_KEYWORDS, - "set_attribute_values(attrname, values) -> list\n--\n\n" + "set_attribute_values(attrname, values)\n--\n\n" "Sets the value of a given vertex attribute for all vertices\n\n" "@param attrname: the name of the attribute\n" "@param values: the new attribute values in a list\n" }, {"select", (PyCFunction)igraphmodule_VertexSeq_select, METH_VARARGS, - "select(...) -> VertexSeq\n--\n\n" + "select(*args, **kwds)\n--\n\n" "For internal use only.\n" }, {"_reindex_names", (PyCFunction)igraphmodule_VertexSeq__reindex_names, METH_NOARGS, - "_reindex_names() -> None\n--\n\n" + "_reindex_names()\n--\n\n" "Re-creates the dictionary that maps vertex names to IDs.\n\n" "For internal use only.\n" }, From d37e883c3274faddf9092a3ce5b31a0beaa8760f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 7 Mar 2021 11:35:17 +0100 Subject: [PATCH 0198/1681] doc: fix a few more docstrings to be compatible with __text_signature__ --- src/_igraph/graphobject.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index a488697b4..13c5d4d7c 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -13974,7 +13974,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_similarity_dice */ {"similarity_dice", (PyCFunction) igraphmodule_Graph_similarity_dice, METH_VARARGS | METH_KEYWORDS, - "similarity_dice(vertices=None, pairs=None, mode=IGRAPH_ALL, loops=True)\n\n" + "similarity_dice(vertices=None, pairs=None, mode=IGRAPH_ALL, loops=True)\n--\n\n" "Dice similarity coefficient of vertices.\n\n" "The Dice similarity coefficient of two vertices is twice the number of\n" "their common neighbors divided by the sum of their degrees. This\n" @@ -14435,7 +14435,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_mds", (PyCFunction) igraphmodule_Graph_layout_mds, METH_VARARGS | METH_KEYWORDS, - "layout_mds(dist=None, dim=2, arpack_options=None)\n--\n" + "layout_mds(dist=None, dim=2, arpack_options=None)\n--\n\n" "Places the vertices in an Euclidean space with the given number of\n" "dimensions using multidimensional scaling.\n\n" "This layout requires a distance matrix, where the intersection of\n" @@ -14468,7 +14468,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_reingold_tilford", (PyCFunction) igraphmodule_Graph_layout_reingold_tilford, METH_VARARGS | METH_KEYWORDS, - "layout_reingold_tilford(mode=\"out\", root=None, rootlevel=None)\n--\n" + "layout_reingold_tilford(mode=\"out\", root=None, rootlevel=None)\n--\n\n" "Places the vertices on a 2D plane according to the Reingold-Tilford\n" "layout algorithm.\n\n" "This is a tree layout. If the given graph is not a tree, a breadth-first\n" @@ -14498,7 +14498,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_reingold_tilford_circular", (PyCFunction) igraphmodule_Graph_layout_reingold_tilford_circular, METH_VARARGS | METH_KEYWORDS, - "layout_reingold_tilford_circular(mode=\"out\", root=None, rootlevel=None)\n--\n" + "layout_reingold_tilford_circular(mode=\"out\", root=None, rootlevel=None)\n--\n\n" "Circular Reingold-Tilford layout for trees.\n\n" "This layout is similar to the Reingold-Tilford layout, but the vertices\n" "are placed in a circular way, with the root vertex in the center.\n\n" @@ -14512,7 +14512,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_layout_random */ {"layout_random", (PyCFunction) igraphmodule_Graph_layout_random, METH_VARARGS | METH_KEYWORDS, - "layout_random(dim=2)\n--\n" + "layout_random(dim=2)\n--\n\n" "Places the vertices of the graph randomly.\n\n" "@param dim: the desired number of dimensions for the layout. dim=2\n" " means a 2D layout, dim=3 means a 3D layout.\n" @@ -15053,8 +15053,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " number of automorphisms if C{other} is C{None}.\n"}, {"get_isomorphisms_vf2", (PyCFunction) igraphmodule_Graph_get_isomorphisms_vf2, METH_VARARGS | METH_KEYWORDS, - "get_isomorphisms_vf2(other=None, color1=None, color2=None, edge_color1=None,\n" - " edge_color2=None, node_compat_fn=None, edge_compat_fn=None)\n\n" + "get_isomorphisms_vf2(other=None, color1=None, color2=None, edge_color1=None, " + "edge_color2=None, node_compat_fn=None, edge_compat_fn=None)\n--\n\n" "Returns all isomorphisms between the graph and another one\n\n" "Vertex and edge colors may be used to restrict the isomorphisms, as only\n" "vertices and edges with the same color will be allowed to match each other.\n\n" @@ -15286,10 +15286,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param loops: whether to include loop edges in the complementer.\n" "@return: the complementer of the graph\n"}, {"compose", (PyCFunction) igraphmodule_Graph_compose, - METH_O, "compose(other)\n\nReturns the composition of two graphs."}, + METH_O, "compose(other)\n--\n\nReturns the composition of two graphs."}, {"difference", (PyCFunction) igraphmodule_Graph_difference, METH_O, - "difference(other)\n\nSubtracts the given graph from the original"}, + "difference(other)\n--\n\nSubtracts the given graph from the original"}, /**********************/ /* DOMINATORS */ From 10ed4ce0c2dbdab67a5d176a6dc4d14a9e934283 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 7 Mar 2021 11:36:58 +0100 Subject: [PATCH 0199/1681] doc: remove typings from text signatures in Edge class --- src/_igraph/edgeobject.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index f42dfa326..c69881899 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -613,17 +613,17 @@ GRAPH_PROXY_METHOD(is_mutual, "is_mutual"); PyMethodDef igraphmodule_Edge_methods[] = { {"attributes", (PyCFunction)igraphmodule_Edge_attributes, METH_NOARGS, - "attributes() -> dict\n--\n\n" + "attributes()\n--\n\n" "Returns a dict of attribute names and values for the edge\n" }, {"attribute_names", (PyCFunction)igraphmodule_Edge_attribute_names, METH_NOARGS, - "attribute_names() -> list\n--\n\n" + "attribute_names()\n--\n\n" "Returns the list of edge attribute names\n" }, {"update_attributes", (PyCFunction)igraphmodule_Edge_update_attributes, METH_VARARGS | METH_KEYWORDS, - "update_attributes(E, **F) -> None\n--\n\n" + "update_attributes(E, **F)\n--\n\n" "Updates the attributes of the edge from dict/iterable E and F.\n\n" "If E has a C{keys()} method, it does: C{for k in E: self[k] = E[k]}.\n" "If E lacks a C{keys()} method, it does: C{for (k, v) in E: self[k] = v}.\n" From 451658d31c7604d393a949c8132fd254b742bed7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Mar 2021 14:43:09 +0100 Subject: [PATCH 0200/1681] doc: documentation fixes for PyDoctor --- doc/source/tutorial.rst | 2 +- src/_igraph/convert.c | 16 ++ src/_igraph/convert.h | 1 + src/_igraph/edgeobject.c | 8 +- src/_igraph/edgeseqobject.c | 2 +- src/_igraph/graphobject.c | 295 ++++++++++++++++----------------- src/_igraph/vertexobject.c | 24 +-- src/_igraph/vertexseqobject.c | 2 +- src/igraph/__init__.py | 302 +++++++++++++++------------------- src/igraph/app/shell.py | 6 +- src/igraph/clustering.py | 13 +- src/igraph/layout.py | 5 +- 12 files changed, 328 insertions(+), 348 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 16ac2c6be..84480fa6c 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -140,7 +140,7 @@ Traceback (most recent call last): File "", line 6, in TypeError: iterable must return pairs of integers or strings -Most |igraph| functions will raise an :exc:`igraph.core.InternalError` if +Most |igraph| functions will raise an :exc:`igraph.InternalError` if something goes wrong. The message corresponding to the exception gives you a short textual explanation of what went wrong (``cannot add edges, invalid vertex id``) along with the corresponding line in the C source where the error diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 762bbeaf9..f92013947 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -476,6 +476,22 @@ int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, return igraphmodule_PyObject_to_enum(o, fas_algorithm_tt, (int*)result); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_get_adjacency_t + */ +int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, + igraph_get_adjacency_t *result) { + static igraphmodule_enum_translation_table_entry_t get_adjacency_tt[] = { + {"lower", IGRAPH_GET_ADJACENCY_LOWER}, + {"upper", IGRAPH_GET_ADJACENCY_UPPER}, + {"both", IGRAPH_GET_ADJACENCY_BOTH}, + {0,0} + }; + + return igraphmodule_PyObject_to_enum(o, get_adjacency_tt, (int*)result); +} + /** * \brief Converts a Python object to an igraph \c igraph_layout_grid_t */ diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 7af034738..d777406d4 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -60,6 +60,7 @@ int igraphmodule_PyObject_to_community_comparison_t(PyObject *obj, int igraphmodule_PyObject_to_connectedness_t(PyObject *o, igraph_connectedness_t *result); int igraphmodule_PyObject_to_degseq_t(PyObject *o, igraph_degseq_t *result); int igraphmodule_PyObject_to_fas_algorithm_t(PyObject *o, igraph_fas_algorithm_t *result); +int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t *result); int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index c69881899..6f3010ee4 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -595,16 +595,16 @@ GRAPH_PROXY_METHOD(is_mutual, "is_mutual"); #define GRAPH_PROXY_METHOD_SPEC(FUNC, METHODNAME) \ {METHODNAME, (PyCFunction)igraphmodule_Edge_##FUNC, METH_VARARGS | METH_KEYWORDS, \ - "Proxy method to L{Graph." METHODNAME "()}\n\n" \ + "Proxy method to L{Graph." METHODNAME "()}\n\n" \ "This method calls the " METHODNAME " method of the L{Graph} class " \ "with this edge as the first argument, and returns the result.\n\n"\ - "@see: Graph." METHODNAME "() for details."} + "@see: L{Graph." METHODNAME "()} for details."} #define GRAPH_PROXY_METHOD_SPEC_2(FUNC, METHODNAME, METHODNAME_IN_GRAPH) \ {METHODNAME, (PyCFunction)igraphmodule_Edge_##FUNC, METH_VARARGS | METH_KEYWORDS, \ - "Proxy method to L{Graph." METHODNAME_IN_GRAPH "()}\n\n" \ + "Proxy method to L{Graph." METHODNAME_IN_GRAPH "()}\n\n" \ "This method calls the " METHODNAME_IN_GRAPH " method of the L{Graph} class " \ "with this edge as the first argument, and returns the result.\n\n"\ - "@see: Graph." METHODNAME_IN_GRAPH "() for details."} + "@see: L{Graph." METHODNAME_IN_GRAPH "()} for details."} /** * \ingroup python_interface_edge diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 507edd3ec..96ee3f19f 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -920,7 +920,7 @@ PyGetSetDef igraphmodule_EdgeSeq_getseters[] = { PyTypeObject igraphmodule_EdgeSeqType = { PyVarObject_HEAD_INIT(0, 0) - "igraph.core.EdgeSeq", /* tp_name */ + "igraph._igraph.EdgeSeq", /* tp_name */ sizeof(igraphmodule_EdgeSeqObject), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)igraphmodule_EdgeSeq_dealloc, /* tp_dealloc */ diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 13c5d4d7c..48cda7d44 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2136,7 +2136,7 @@ PyObject *igraphmodule_Graph_Degree_Sequence(PyTypeObject * type, igraph_bool_t has_inseq = 0; PyObject *outdeg = NULL, *indeg = NULL, *method = NULL; - static char *kwlist[] = { "out", "in", "method", NULL }; + static char *kwlist[] = { "out", "in_", "method", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O!O", kwlist, &PyList_Type, &outdeg, @@ -2632,7 +2632,7 @@ PyObject *igraphmodule_Graph_Isoclass(PyTypeObject * type, igraphmodule_GraphObject *self; igraph_t g; - static char *kwlist[] = { "n", "class", "directed", NULL }; + static char *kwlist[] = { "n", "cls", "directed", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|O", kwlist, &n, &isoclass, &directed)) @@ -7497,19 +7497,14 @@ PyObject *igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "type", "eids", NULL }; - igraph_get_adjacency_t t = IGRAPH_GET_ADJACENCY_BOTH; + igraph_get_adjacency_t mode = IGRAPH_GET_ADJACENCY_BOTH; igraph_matrix_t m; - PyObject *result, *eids = Py_False; + PyObject *result, *mode_o = Py_None, *eids = Py_False; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", kwlist, &t, &eids)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &eids)) return NULL; - if (t != IGRAPH_GET_ADJACENCY_UPPER && t != IGRAPH_GET_ADJACENCY_LOWER && - t != IGRAPH_GET_ADJACENCY_BOTH) { - PyErr_SetString(PyExc_ValueError, - "type must be either GET_ADJACENCY_LOWER or GET_ADJACENCY_UPPER or GET_ADJACENCY_BOTH"); - return NULL; - } + if (igraphmodule_PyObject_to_get_adjacency_t(mode_o, &mode)) return NULL; if (igraph_matrix_init (&m, igraph_vcount(&self->g), igraph_vcount(&self->g))) { @@ -7517,7 +7512,7 @@ PyObject *igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject * self, return NULL; } - if (igraph_get_adjacency(&self->g, &m, t, PyObject_IsTrue(eids))) { + if (igraph_get_adjacency(&self->g, &m, mode, PyObject_IsTrue(eids))) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&m); return NULL; @@ -12023,7 +12018,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_add_vertices */ {"add_vertices", (PyCFunction) igraphmodule_Graph_add_vertices, METH_VARARGS, - "add_vertices(n: int)\n--\n\n" + "add_vertices(n)\n--\n\n" "Adds vertices to the graph.\n\n" "@param n: the number of vertices to be added\n"}, @@ -12058,7 +12053,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_degree */ {"degree", (PyCFunction) igraphmodule_Graph_degree, METH_VARARGS | METH_KEYWORDS, - "degree(vertices, mode=ALL, loops=True)\n--\n\n" + "degree(vertices, mode=\"all\", loops=True)\n--\n\n" "Returns some vertex degrees from the graph.\n\n" "This method accepts a single vertex ID or a list of vertex IDs as a\n" "parameter, and returns the degree of the given vertices (in the\n" @@ -12066,14 +12061,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "parameter).\n" "\n" "@param vertices: a single vertex ID or a list of vertex IDs\n" - "@param mode: the type of degree to be returned (L{OUT} for\n" - " out-degrees, L{IN} IN for in-degrees or L{ALL} for the sum of\n" - " them).\n" "@param loops: whether self-loops should be counted.\n"}, + "@param mode: the type of degree to be returned (C{\"out\"} for\n" + " out-degrees, C{\"in\"} for in-degrees or C{\"all\"} for the sum of\n" + " them).\n" + "@param loops: whether self-loops should be counted.\n"}, /* interface to igraph_strength */ {"strength", (PyCFunction) igraphmodule_Graph_strength, METH_VARARGS | METH_KEYWORDS, - "strength(vertices, mode=ALL, loops=True, weights=None)\n--\n\n" + "strength(vertices, mode=\"all\", loops=True, weights=None)\n--\n\n" "Returns the strength (weighted degree) of some vertices from the graph\n\n" "This method accepts a single vertex ID or a list of vertex IDs as a\n" "parameter, and returns the strength (that is, the sum of the weights\n" @@ -12082,8 +12078,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "parameter).\n" "\n" "@param vertices: a single vertex ID or a list of vertex IDs\n" - "@param mode: the type of degree to be returned (L{OUT} for\n" - " out-degrees, L{IN} IN for in-degrees or L{ALL} for the sum of\n" + "@param mode: the type of degree to be returned (C{\"out\"} for\n" + " out-degrees, C{\"in\"} for in-degrees or C{\"all\"} for the sum of\n" " them).\n" "@param loops: whether self-loops should be counted.\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" @@ -12155,24 +12151,24 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighbors */ {"neighbors", (PyCFunction) igraphmodule_Graph_neighbors, METH_VARARGS | METH_KEYWORDS, - "neighbors(vertex, mode=ALL)\n--\n\n" + "neighbors(vertex, mode=\"all\")\n--\n\n" "Returns adjacent vertices to a given vertex.\n\n" "@param vertex: a vertex ID\n" - "@param mode: whether to return only successors (L{OUT}),\n" - " predecessors (L{IN}) or both (L{ALL}). Ignored for undirected\n" + "@param mode: whether to return only successors (C{\"out\"}),\n" + " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" " graphs."}, {"successors", (PyCFunction) igraphmodule_Graph_successors, METH_VARARGS | METH_KEYWORDS, "successors(vertex)\n--\n\n" "Returns the successors of a given vertex.\n\n" - "Equivalent to calling the L{Graph.neighbors} method with type=L{OUT}."}, + "Equivalent to calling the L{neighbors()} method with type=C{\"out\"}."}, {"predecessors", (PyCFunction) igraphmodule_Graph_predecessors, METH_VARARGS | METH_KEYWORDS, "predecessors(vertex)\n--\n\n" "Returns the predecessors of a given vertex.\n\n" - "Equivalent to calling the L{Graph.neighbors} method with type=L{IN}."}, + "Equivalent to calling the L{neighbors()} method with type=C{\"in\"}."}, /* interface to igraph_get_eid */ {"get_eid", (PyCFunction) igraphmodule_Graph_get_eid, @@ -12220,11 +12216,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_incident */ {"incident", (PyCFunction) igraphmodule_Graph_incident, METH_VARARGS | METH_KEYWORDS, - "incident(vertex, mode=OUT)\n--\n\n" + "incident(vertex, mode=\"out\")\n--\n\n" "Returns the edges a given vertex is incident on.\n\n" "@param vertex: a vertex ID\n" - "@param mode: whether to return only successors (L{OUT}),\n" - " predecessors (L{IN}) or both (L{ALL}). Ignored for undirected\n" + "@param mode: whether to return only successors (C{\"out\"}),\n" + " predecessors (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" " graphs."}, ////////////////////// @@ -12234,24 +12230,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_adjacency */ {"Adjacency", (PyCFunction) igraphmodule_Graph_Adjacency, METH_CLASS | METH_VARARGS | METH_KEYWORDS, - "Adjacency(matrix, mode=ADJ_DIRECTED)\n--\n\n" + "Adjacency(matrix, mode=\"directed\")\n--\n\n" "Generates a graph from its adjacency matrix.\n\n" "@param matrix: the adjacency matrix\n" "@param mode: the mode to be used. Possible values are:\n" "\n" - " - C{ADJ_DIRECTED} - the graph will be directed and a matrix\n" - " element gives the number of edges between two vertex.\n" - " - C{ADJ_UNDIRECTED} - alias to C{ADJ_MAX} for convenience.\n" - " - C{ADJ_MAX} - undirected graph will be created and the number of\n" + " - C{\"directed\"} - the graph will be directed and a matrix\n" + " element gives the number of edges between two vertices.\n" + " - C{\"undirected\"} - alias to C{\"max\"} for convenience.\n" + " - C{\"max\"} - undirected graph will be created and the number of\n" " edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))}\n" - " - C{ADJ_MIN} - like C{ADJ_MAX}, but with M{min(A(i,j), A(j,i))}\n" - " - C{ADJ_PLUS} - like C{ADJ_MAX}, but with M{A(i,j) + A(j,i)}\n" - " - C{ADJ_UPPER} - undirected graph with the upper right triangle of\n" + " - C{\"min\"} - like C{\"max\"}, but with M{min(A(i,j), A(j,i))}\n" + " - C{\"plus\"} - like C{\"max\"}, but with M{A(i,j) + A(j,i)}\n" + " - C{\"upper\"} - undirected graph with the upper right triangle of\n" " the matrix (including the diagonal)\n" - " - C{ADJ_LOWER} - undirected graph with the lower left triangle of\n" + " - C{\"lower\"} - undirected graph with the lower left triangle of\n" " the matrix (including the diagonal)\n" - "\n" - " These values can also be given as strings without the C{ADJ} prefix.\n" }, /* interface to igraph_asymmetric_preference_game */ @@ -12260,7 +12254,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Asymmetric_Preference(n, type_dist_matrix, pref_matrix, attribute=None, loops=False)\n--\n\n" "Generates a graph based on asymmetric vertex types and connection probabilities.\n\n" - "This is the asymmetric variant of L{Graph.Preference}.\n" + "This is the asymmetric variant of L{Preference()}.\n" "A given number of vertices are generated. Every vertex is assigned to an\n" "\"incoming\" and an \"outgoing\" vertex type according to the given joint\n" "type probabilities. Finally, every vertex pair is evaluated and a\n" @@ -12279,7 +12273,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_atlas {"Atlas", (PyCFunction) igraphmodule_Graph_Atlas, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Atlas(idx: int)\n--\n\n" + "Atlas(idx)\n--\n\n" "Generates a graph from the Graph Atlas.\n\n" "@param idx: The index of the graph to be generated.\n" " Indices start from zero, graphs are listed:\n\n" @@ -12330,7 +12324,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " it will generate multiple edges as well. igraph before\n" " 0.6 used this algorithm for I{power}s other than 1.\n\n" "@param start_from: if given and not C{None}, this must be another\n" - " L{Graph} object. igraph will use this graph as a starting\n" + " L{GraphBase} object. igraph will use this graph as a starting\n" " point for the preferential attachment model.\n\n" "@newfield ref: Reference\n" "@ref: Barabasi, A-L and Albert, R. 1999. Emergence of scaling\n" @@ -12468,7 +12462,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_incidence */ {"_Incidence", (PyCFunction) igraphmodule_Graph_Incidence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "_Incidence(matrix, directed=False, mode=ALL, multiple=False)\n--\n\n" + "_Incidence(matrix, directed=False, mode=\"all\", multiple=False)\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.Incidence()\n\n"}, @@ -12508,7 +12502,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_CLASS | METH_KEYWORDS, "Preference(n, type_dist, pref_matrix, attribute=None, directed=False, loops=False)\n--\n\n" "Generates a graph based on vertex types and connection probabilities.\n\n" - "This is practically the nongrowing variant of L{Graph.Establishment}.\n" + "This is practically the nongrowing variant of L{Establishment}.\n" "A given number of vertices are generated. Every vertex is assigned to a\n" "vertex type according to the given type probabilities. Finally, every\n" "vertex pair is evaluated and an edge is created between them with a\n" @@ -12677,24 +12671,24 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_tree {"Tree", (PyCFunction) igraphmodule_Graph_Tree, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Tree(n, children, type=TREE_UNDIRECTED)\n--\n\n" + "Tree(n, children, type=\"undirected\")\n--\n\n" "Generates a tree in which almost all vertices have the same number of children.\n\n" "@param n: the number of vertices in the graph\n" "@param children: the number of children of a vertex in the graph\n" "@param type: determines whether the tree should be directed, and if\n" " this is the case, also its orientation. Must be one of\n" - " C{TREE_IN}, C{TREE_OUT} and C{TREE_UNDIRECTED}.\n"}, + " C{\"in\"}, C{\"out\"} and C{\"undirected\"}.\n"}, /* interface to igraph_degree_sequence_game */ {"Degree_Sequence", (PyCFunction) igraphmodule_Graph_Degree_Sequence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Degree_Sequence(out, in=None, method=\"simple\")\n--\n\n" + "Degree_Sequence(out, in_=None, method=\"simple\")\n--\n\n" "Generates a graph with a given degree sequence.\n\n" "@param out: the out-degree sequence for a directed graph. If the\n" " in-degree sequence is omitted, the generated graph\n" " will be undirected, so this will be the in-degree\n" " sequence as well\n" - "@param in: the in-degree sequence for a directed graph.\n" + "@param in_: the in-degree sequence for a directed graph.\n" " If omitted, the generated graph will be undirected.\n" "@param method: the generation method to be used. One of the following:\n" " \n" @@ -12722,10 +12716,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_isoclass_create */ {"Isoclass", (PyCFunction) igraphmodule_Graph_Isoclass, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Isoclass(n, class, directed=False)\n--\n\n" + "Isoclass(n, cls, directed=False)\n--\n\n" "Generates a graph with a given isomorphism class.\n\n" "@param n: the number of vertices in the graph (3 or 4)\n" - "@param class: the isomorphism class\n" + "@param cls: the isomorphism class\n" "@param directed: whether the graph should be directed.\n"}, /* interface to igraph_watts_strogatz_game */ @@ -12748,24 +12742,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_weighted_adjacency */ {"Weighted_Adjacency", (PyCFunction) igraphmodule_Graph_Weighted_Adjacency, METH_CLASS | METH_VARARGS | METH_KEYWORDS, - "Weighted_Adjacency(matrix, mode=ADJ_DIRECTED, attr=\"weight\", loops=True)\n--\n\n" + "Weighted_Adjacency(matrix, mode=\"directed\", attr=\"weight\", loops=True)\n--\n\n" "Generates a graph from its adjacency matrix.\n\n" "@param matrix: the adjacency matrix\n" "@param mode: the mode to be used. Possible values are:\n" "\n" - " - C{ADJ_DIRECTED} - the graph will be directed and a matrix\n" - " element gives the number of edges between two vertex.\n" - " - C{ADJ_UNDIRECTED} - alias to C{ADJ_MAX} for convenience.\n" - " - C{ADJ_MAX} - undirected graph will be created and the number of\n" + " - C{\"directed\"} - the graph will be directed and a matrix\n" + " element gives the number of edges between two vertices.\n" + " - C{\"undirected\"} - alias to C{\"max\"} for convenience.\n" + " - C{\"max\"} - undirected graph will be created and the number of\n" " edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))}\n" - " - C{ADJ_MIN} - like C{ADJ_MAX}, but with M{min(A(i,j), A(j,i))}\n" - " - C{ADJ_PLUS} - like C{ADJ_MAX}, but with M{A(i,j) + A(j,i)}\n" - " - C{ADJ_UPPER} - undirected graph with the upper right triangle of\n" + " - C{\"min\"} - like C{\"max\"}, but with M{min(A(i,j), A(j,i))}\n" + " - C{\"plus\"} - like C{\"max\"}, but with M{A(i,j) + A(j,i)}\n" + " - C{\"upper\"} - undirected graph with the upper right triangle of\n" " the matrix (including the diagonal)\n" - " - C{ADJ_LOWER} - undirected graph with the lower left triangle of\n" + " - C{\"lower\"} - undirected graph with the lower left triangle of\n" " the matrix (including the diagonal)\n" - "\n" - " These values can also be given as strings without the C{ADJ} prefix.\n" "@param attr: the name of the edge attribute that stores the edge\n" " weights.\n" "@param loops: whether to include loop edges. When C{False}, the diagonal\n" @@ -12949,7 +12941,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_closeness */ {"closeness", (PyCFunction) igraphmodule_Graph_closeness, METH_VARARGS | METH_KEYWORDS, - "closeness(vertices=None, mode=ALL, cutoff=None, weights=None, " + "closeness(vertices=None, mode=\"all\", cutoff=None, weights=None, " "normalized=True)\n--\n\n" "Calculates the closeness centralities of given vertices in a graph.\n\n" "The closeness centerality of a vertex measures how easily other\n" @@ -12963,9 +12955,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "geodesic.\n\n" "@param vertices: the vertices for which the closenesses must\n" " be returned. If C{None}, uses all of the vertices in the graph.\n" - "@param mode: must be one of L{IN}, L{OUT} and L{ALL}. L{IN} means\n" - " that the length of the incoming paths, L{OUT} means that the\n" - " length of the outgoing paths must be calculated. L{ALL} means\n" + "@param mode: must be one of C{\"in\"}, C{\"out\"} and C{\"all\"}. C{\"in\"} means\n" + " that the length of the incoming paths, C{\"out\"} means that the\n" + " length of the outgoing paths must be calculated. C{\"all\"} means\n" " that both of them must be calculated.\n" "@param cutoff: if it is an integer, only paths less than or equal to this\n" " length are considered, effectively resulting in an estimation of the\n" @@ -12982,7 +12974,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_harmonic_centrality */ {"harmonic_centrality", (PyCFunction) igraphmodule_Graph_harmonic_centrality, METH_VARARGS | METH_KEYWORDS, - "harmonic_centrality(vertices=None, mode=ALL, cutoff=None, weights=None, " + "harmonic_centrality(vertices=None, mode=\"all\", cutoff=None, weights=None, " "normalized=True)\n--\n\n" "Calculates the harmonic centralities of given vertices in a graph.\n\n" "The harmonic centerality of a vertex measures how easily other\n" @@ -12993,9 +12985,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "vertices, the inverse distance is taken to be zero.\n\n" "@param vertices: the vertices for which the harmonic centrality must\n" " be returned. If C{None}, uses all of the vertices in the graph.\n" - "@param mode: must be one of L{IN}, L{OUT} and L{ALL}. L{IN} means\n" - " that the length of the incoming paths, L{OUT} means that the\n" - " length of the outgoing paths must be calculated. L{ALL} means\n" + "@param mode: must be one of C{\"in\"}, C{\"out\"} and C{\"all\"}. C{\"in\"} means\n" + " that the length of the incoming paths, C{\"out\"} means that the\n" + " length of the outgoing paths must be calculated. C{\"all\"} means\n" " that both of them must be calculated.\n" "@param cutoff: if it is not C{None}, only paths less than or equal to this\n" " length are considered.\n" @@ -13010,13 +13002,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_clusters */ {"clusters", (PyCFunction) igraphmodule_Graph_clusters, METH_VARARGS | METH_KEYWORDS, - "clusters(mode=STRONG)\n--\n\n" + "clusters(mode=\"strong\")\n--\n\n" "Calculates the (strong or weak) clusters for a given graph.\n\n" "@attention: this function has a more convenient interface in class\n" " L{Graph} which wraps the result in a L{VertexClustering} object.\n" " It is advised to use that.\n" - "@param mode: must be either C{STRONG} or C{WEAK}, depending on\n" - " the clusters being sought. Optional, defaults to C{STRONG}.\n" + "@param mode: must be either C{\"strong\"} or C{\"weak\"}, depending on\n" + " the clusters being sought. Optional, defaults to C{\"strong\"}.\n" "@return: the component index for every node in the graph.\n"}, {"copy", (PyCFunction) igraphmodule_Graph_copy, METH_NOARGS, @@ -13029,10 +13021,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"decompose", (PyCFunction) igraphmodule_Graph_decompose, METH_VARARGS | METH_KEYWORDS, - "decompose(mode=STRONG, maxcompno=None, minelements=1)\n--\n\n" + "decompose(mode=\"strong\", maxcompno=None, minelements=1)\n--\n\n" "Decomposes the graph into subgraphs.\n\n" - "@param mode: must be either STRONG or WEAK, depending on the\n" - " clusters being sought.\n" + "@param mode: must be either C{\"strong\"} or C{\"weak\"}, depending on\n" + " the clusters being sought. Optional, defaults to C{\"strong\"}.\n" "@param maxcompno: maximum number of components to return.\n" " C{None} means all possible components.\n" "@param minelements: minimum number of vertices in a component.\n" @@ -13061,9 +13053,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " C{first}, C{last}, C{random}. You can also specify different\n" " combination functions for different attributes by passing a dict\n" " here which maps attribute names to functions. See\n" - " L{Graph.simplify()} for more details.\n" + " L{simplify()} for more details.\n" "@return: C{None}.\n" - "@see: L{Graph.simplify()}\n" + "@see: L{simplify()}\n" }, /* interface to igraph_constraint */ {"constraint", (PyCFunction) igraphmodule_Graph_constraint, @@ -13167,16 +13159,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_eccentricity */ {"eccentricity", (PyCFunction) igraphmodule_Graph_eccentricity, METH_VARARGS | METH_KEYWORDS, - "eccentricity(vertices=None, mode=ALL)\n--\n\n" + "eccentricity(vertices=None, mode=\"all\")\n--\n\n" "Calculates the eccentricities of given vertices in a graph.\n\n" "The eccentricity of a vertex is calculated by measuring the\n" "shortest distance from (or to) the vertex, to (or from) all other\n" "vertices in the graph, and taking the maximum.\n\n" "@param vertices: the vertices for which the eccentricity scores must\n" " be returned. If C{None}, uses all of the vertices in the graph.\n" - "@param mode: must be one of L{IN}, L{OUT} and L{ALL}. L{IN} means\n" - " that edge directions are followed; C{OUT} means that edge directions\n" - " are followed the opposite direction; C{ALL} means that directions are\n" + "@param mode: must be one of C{\"in\"}, C{\"out\"} and C{\"all\"}. C{\"in\"} means\n" + " that edge directions are followed; C{\"out\"} means that edge directions\n" + " are followed the opposite direction; C{\"all\"} means that directions are\n" " ignored. The argument has no effect for undirected graphs.\n" "@return: the calculated eccentricities in a list, or a single number if\n" " a single vertex was supplied.\n"}, @@ -13293,7 +13285,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_get_shortest_paths {"get_shortest_paths", (PyCFunction) igraphmodule_Graph_get_shortest_paths, METH_VARARGS | METH_KEYWORDS, - "get_shortest_paths(v, to=None, weights=None, mode=OUT, output=\"vpath\")\n--\n\n" + "get_shortest_paths(v, to=None, weights=None, mode=\"out\", output=\"vpath\")\n--\n\n" "Calculates the shortest paths from/to a given node in a graph.\n\n" "@param v: the source/destination for the calculated paths\n" "@param to: a vertex selector describing the destination/source for\n" @@ -13303,13 +13295,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param weights: edge weights in a list or the name of an edge attribute\n" " holding edge weights. If C{None}, all edges are assumed to have\n" " equal weight.\n" - "@param mode: the directionality of the paths. L{IN} means to\n" - " calculate incoming paths, L{OUT} means to calculate outgoing\n" - " paths, L{ALL} means to calculate both ones.\n" + "@param mode: the directionality of the paths. C{\"in\"} means to\n" + " calculate incoming paths, C{\"out\"} means to calculate outgoing\n" + " paths, C{\"all\"} means to calculate both ones.\n" "@param output: determines what should be returned. If this is\n" " C{\"vpath\"}, a list of vertex IDs will be returned, one path\n" " for each target vertex. For unconnected graphs, some of the list\n" - " elements may be empty. Note that in case of mode=L{IN}, the vertices\n" + " elements may be empty. Note that in case of mode=C{\"in\"}, the vertices\n" " in a path are returned in reversed order. If C{output=\"epath\"},\n" " edge IDs are returned instead of vertex IDs.\n" "@return: see the documentation of the C{output} parameter.\n"}, @@ -13318,7 +13310,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"get_all_shortest_paths", (PyCFunction) igraphmodule_Graph_get_all_shortest_paths, METH_VARARGS | METH_KEYWORDS, - "get_all_shortest_paths(v, to=None, weights=None, mode=OUT)\n--\n\n" + "get_all_shortest_paths(v, to=None, weights=None, mode=\"out\")\n--\n\n" "Calculates all of the shortest paths from/to a given node in a graph.\n\n" "@param v: the source for the calculated paths\n" "@param to: a vertex selector describing the destination for\n" @@ -13328,18 +13320,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param weights: edge weights in a list or the name of an edge attribute\n" " holding edge weights. If C{None}, all edges are assumed to have\n" " equal weight.\n" - "@param mode: the directionality of the paths. L{IN} means to\n" - " calculate incoming paths, L{OUT} means to calculate outgoing\n" - " paths, L{ALL} means to calculate both ones.\n" + "@param mode: the directionality of the paths. C{\"in\"} means to\n" + " calculate incoming paths, C{\"out\"} means to calculate outgoing\n" + " paths, C{\"all\"} means to calculate both ones.\n" "@return: all of the shortest path from the given node to every other\n" - " reachable node in the graph in a list. Note that in case of mode=L{IN},\n" + " reachable node in the graph in a list. Note that in case of mode=C{\"in\"},\n" " the vertices in a path are returned in reversed order!"}, /* interface to igraph_get_all_simple_paths */ {"_get_all_simple_paths", (PyCFunction) igraphmodule_Graph_get_all_simple_paths, METH_VARARGS | METH_KEYWORDS, - "_get_all_simple_paths(v, to=None, cutoff=-1, mode=OUT)\n--\n\n" + "_get_all_simple_paths(v, to=None, cutoff=-1, mode=\"out\")\n--\n\n" "Internal function, undocumented.\n\n" "@see: Graph.get_all_simple_paths()\n\n" }, @@ -13450,7 +13442,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_is_connected */ {"is_connected", (PyCFunction) igraphmodule_Graph_is_connected, METH_VARARGS | METH_KEYWORDS, - "is_connected(mode=STRONG)\n--\n\n" + "is_connected(mode=\"strong\")\n--\n\n" "Decides whether the graph is connected.\n\n" "@param mode: whether we should calculate strong or weak connectivity.\n" "@return: C{True} if the graph is connected, C{False} otherwise.\n"}, @@ -13473,7 +13465,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_maxdegree */ {"maxdegree", (PyCFunction) igraphmodule_Graph_maxdegree, METH_VARARGS | METH_KEYWORDS, - "maxdegree(vertices=None, mode=ALL, loops=False)\n--\n\n" + "maxdegree(vertices=None, mode=\"all\", loops=False)\n--\n\n" "Returns the maximum degree of a vertex set in the graph.\n\n" "This method accepts a single vertex ID or a list of vertex IDs as a\n" "parameter, and returns the degree of the given vertices (in the\n" @@ -13482,15 +13474,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "\n" "@param vertices: a single vertex ID or a list of vertex IDs, or\n" " C{None} meaning all the vertices in the graph.\n" - "@param mode: the type of degree to be returned (L{OUT} for\n" - " out-degrees, L{IN} IN for in-degrees or L{ALL} for the sum of\n" + "@param mode: the type of degree to be returned (C{\"out\"} for\n" + " out-degrees, C{\"in\"} IN for in-degrees or C{\"all\"} for the sum of\n" " them).\n" "@param loops: whether self-loops should be counted.\n"}, /* interface to igraph_neighborhood */ {"neighborhood", (PyCFunction) igraphmodule_Graph_neighborhood, METH_VARARGS | METH_KEYWORDS, - "neighborhood(vertices=None, order=1, mode=ALL, mindist=0)\n--\n\n" + "neighborhood(vertices=None, order=1, mode=\"all\", mindist=0)\n--\n\n" "For each vertex specified by I{vertices}, returns the\n" "vertices reachable from that vertex in at most I{order} steps. If\n" "I{mindist} is larger than zero, vertices that are reachable in less\n" @@ -13518,7 +13510,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_neighborhood_size */ {"neighborhood_size", (PyCFunction) igraphmodule_Graph_neighborhood_size, METH_VARARGS | METH_KEYWORDS, - "neighborhood_size(vertices=None, order=1, mode=ALL, mindist=0)\n--\n\n" + "neighborhood_size(vertices=None, order=1, mode=\"all\", mindist=0)\n--\n\n" "For each vertex specified by I{vertices}, returns the number of\n" "vertices reachable from that vertex in at most I{order} steps. If\n" "I{mindist} is larger than zero, vertices that are reachable in less\n" @@ -13624,7 +13616,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interfaces to igraph_radius */ {"radius", (PyCFunction) igraphmodule_Graph_radius, METH_VARARGS | METH_KEYWORDS, - "radius(mode=OUT)\n--\n\n" + "radius(mode=\"out\")\n--\n\n" "Calculates the radius of the graph.\n\n" "The radius of a graph is defined as the minimum eccentricity of\n" "its vertices (see L{eccentricity()}).\n" @@ -13634,7 +13626,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " edge directions, C{ALL} ignores edge directions. The argument is\n" " ignored for undirected graphs.\n" "@return: the radius\n" - "@see: L{Graph.eccentricity()}" + "@see: L{eccentricity()}" }, /* interface to igraph_reciprocity */ @@ -13692,7 +13684,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_shortest_paths */ {"shortest_paths", (PyCFunction) igraphmodule_Graph_shortest_paths, METH_VARARGS | METH_KEYWORDS, - "shortest_paths(source=None, target=None, weights=None, mode=OUT)\n--\n\n" + "shortest_paths(source=None, target=None, weights=None, mode=\"out\")\n--\n\n" "Calculates shortest path lengths for given vertices in a graph.\n\n" "The algorithm used for the calculations is selected automatically:\n" "a simple BFS is used for unweighted graphs, Dijkstra's algorithm is\n" @@ -13707,8 +13699,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " an attribute name (edge weights are retrieved from the given\n" " attribute) or C{None} (all edges have equal weight).\n" "@param mode: the type of shortest paths to be used for the\n" - " calculation in directed graphs. L{OUT} means only outgoing,\n" - " L{IN} means only incoming paths. L{ALL} means to consider\n" + " calculation in directed graphs. C{\"out\"} means only outgoing,\n" + " C{\"in\"} means only incoming paths. C{\"all\"} means to consider\n" " the directed graph as an undirected one.\n" "@return: the shortest path lengths for given vertices in a matrix\n"}, @@ -13774,16 +13766,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_subcomponent {"subcomponent", (PyCFunction) igraphmodule_Graph_subcomponent, METH_VARARGS | METH_KEYWORDS, - "subcomponent(v, mode=ALL)\n--\n\n" + "subcomponent(v, mode=\"all\")\n--\n\n" "Determines the indices of vertices which are in the same component as a given vertex.\n\n" "@param v: the index of the vertex used as the source/destination\n" - "@param mode: if equals to L{IN}, returns the vertex IDs from\n" - " where the given vertex can be reached. If equals to L{OUT},\n" + "@param mode: if equals to C{\"in\"}, returns the vertex IDs from\n" + " where the given vertex can be reached. If equals to C{\"out\"},\n" " returns the vertex IDs which are reachable from the given\n" - " vertex. If equals to L{ALL}, returns all vertices within the\n" + " vertex. If equals to C{\"all\"}, returns all vertices within the\n" " same component as the given vertex, ignoring edge directions.\n" " Note that this is not equal to calculating the union of the \n" - " results of L{IN} and L{OUT}.\n" + " results of C{\"in\"} and C{\"out\"}.\n" "@return: the indices of vertices which are in the same component as a given vertex.\n"}, /* interface to igraph_subgraph_edges */ @@ -13802,13 +13794,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"topological_sorting", (PyCFunction) igraphmodule_Graph_topological_sorting, METH_VARARGS | METH_KEYWORDS, - "topological_sorting(mode=OUT)\n--\n\n" + "topological_sorting(mode=\"out\")\n--\n\n" "Calculates a possible topological sorting of the graph.\n\n" "Returns a partial sorting and issues a warning if the graph is not\n" "a directed acyclic graph.\n\n" - "@param mode: if L{OUT}, vertices are returned according to the\n" + "@param mode: if C{\"out\"}, vertices are returned according to the\n" " forward topological order -- all vertices come before their\n" - " successors. If L{IN}, all vertices come before their ancestors.\n" + " successors. If C{\"in\"}, all vertices come before their ancestors.\n" "@return: a possible topological ordering as a list"}, /* interface to to_prufer */ @@ -13909,11 +13901,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_unfold_tree */ {"unfold_tree", (PyCFunction) igraphmodule_Graph_unfold_tree, METH_VARARGS | METH_KEYWORDS, - "unfold_tree(sources=None, mode=OUT)\n--\n\n" + "unfold_tree(sources=None, mode=\"out\")\n--\n\n" "Unfolds the graph using a BFS to a tree by duplicating vertices as necessary.\n\n" "@param sources: the source vertices to start the unfolding from. It should be a\n" " list of vertex indices, preferably one vertex from each connected component.\n" - " You can use L{Graph.topological_sorting()} to determine a suitable set. A single\n" + " You can use L{topological_sorting()} to determine a suitable set. A single\n" " vertex index is also accepted.\n" "@param mode: which edges to follow during the BFS. C{OUT} follows outgoing edges,\n" " C{IN} follows incoming edges, C{ALL} follows both. Ignored for undirected\n" @@ -13974,7 +13966,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_similarity_dice */ {"similarity_dice", (PyCFunction) igraphmodule_Graph_similarity_dice, METH_VARARGS | METH_KEYWORDS, - "similarity_dice(vertices=None, pairs=None, mode=IGRAPH_ALL, loops=True)\n--\n\n" + "similarity_dice(vertices=None, pairs=None, mode=\"all\", loops=True)\n--\n\n" "Dice similarity coefficient of vertices.\n\n" "The Dice similarity coefficient of two vertices is twice the number of\n" "their common neighbors divided by the sum of their degrees. This\n" @@ -13986,7 +13978,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " must be C{None}, and the similarity values will be calculated only for the\n" " given pairs. Vertex pairs must be specified as tuples of vertex IDs.\n" "@param mode: which neighbors should be considered for directed graphs.\n" - " Can be L{ALL}, L{IN} or L{OUT}, ignored for undirected graphs.\n" + " Can be C{\"all\"}, C{\"in\"} or C{\"out\"}, ignored for undirected graphs.\n" "@param loops: whether vertices should be considered adjacent to\n" " themselves. Setting this to C{True} assumes a loop edge for all vertices\n" " even if none is present in the graph. Setting this to C{False} may\n" @@ -14001,7 +13993,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"similarity_inverse_log_weighted", (PyCFunction) igraphmodule_Graph_similarity_inverse_log_weighted, METH_VARARGS | METH_KEYWORDS, - "similarity_inverse_log_weighted(vertices=None, mode=IGRAPH_ALL)\n--\n\n" + "similarity_inverse_log_weighted(vertices=None, mode=\"all\")\n--\n\n" "Inverse log-weighted similarity coefficient of vertices.\n\n" "Each vertex is assigned a weight which is 1 / log(degree). The\n" "log-weighted similarity of two vertices is the sum of the weights\n" @@ -14009,8 +14001,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param vertices: the vertices to be analysed. If C{None}, all vertices\n" " will be considered.\n" "@param mode: which neighbors should be considered for directed graphs.\n" - " Can be L{ALL}, L{IN} or L{OUT}, ignored for undirected graphs.\n" - " L{IN} means that the weights are determined by the out-degrees, L{OUT}\n" + " Can be C{\"all\"}, C{\"in\"} or C{\"out\"}, ignored for undirected graphs.\n" + " C{\"in\"} means that the weights are determined by the out-degrees, C{\"out\"}\n" " means that the weights are determined by the in-degrees.\n" "@return: the pairwise similarity coefficients for the vertices specified,\n" " in the form of a matrix (list of lists).\n" @@ -14018,7 +14010,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_similarity_jaccard */ {"similarity_jaccard", (PyCFunction) igraphmodule_Graph_similarity_jaccard, METH_VARARGS | METH_KEYWORDS, - "similarity_jaccard(vertices=None, pairs=None, mode=IGRAPH_ALL, loops=True)\n--\n\n" + "similarity_jaccard(vertices=None, pairs=None, mode=\"all\", loops=True)\n--\n\n" "Jaccard similarity coefficient of vertices.\n\n" "The Jaccard similarity coefficient of two vertices is the number of their\n" "common neighbors divided by the number of vertices that are adjacent to\n" @@ -14029,7 +14021,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " must be C{None}, and the similarity values will be calculated only for the\n" " given pairs. Vertex pairs must be specified as tuples of vertex IDs.\n" "@param mode: which neighbors should be considered for directed graphs.\n" - " Can be L{ALL}, L{IN} or L{OUT}, ignored for undirected graphs.\n" + " Can be C{\"all\"}, C{\"in\"} or C{\"out\"}, ignored for undirected graphs.\n" "@param loops: whether vertices should be considered adjacent to\n" " themselves. Setting this to C{True} assumes a loop edge for all vertices\n" " even if none is present in the graph. Setting this to C{False} may\n" @@ -14070,7 +14062,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param callback: C{None} or a callable that will be called for every motif\n" " found in the graph. The callable must accept three parameters: the graph\n" " itself, the list of vertices in the motif and the isomorphism class of the\n" - " motif (see L{Graph.isoclass()}). The search will stop when the callback\n" + " motif (see L{isoclass()}). The search will stop when the callback\n" " returns an object with a non-zero truth value or raises an exception.\n" "@return: the list of motifs if I{callback} is C{None}, or C{None} otherwise\n" "@see: Graph.motifs_randesu_no()\n" @@ -14094,7 +14086,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"motifs_randesu_estimate", (PyCFunction) igraphmodule_Graph_motifs_randesu_estimate, METH_VARARGS | METH_KEYWORDS, - "motifs_randesu_estimate(size=3, cut_prob=None, sample)\n--\n\n" + "motifs_randesu_estimate(size=3, cut_prob=None, sample=None)\n--\n\n" "Counts the total number of motifs in the graph\n\n" "Motifs are small subgraphs of a given structure in a graph.\n" "This function estimates the total number of motifs in a graph without\n" @@ -14206,9 +14198,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"layout_kamada_kawai", (PyCFunction) igraphmodule_Graph_layout_kamada_kawai, METH_VARARGS | METH_KEYWORDS, - "layout_kamada_kawai(maxiter=1000, seed=None, maxiter=1000, epsilon=0, " - "kkconst=None, minx=None, maxx=None, miny=None, maxy=None, " - "minz=None, maxz=None, dim=2)\n--\n\n" + "layout_kamada_kawai(maxiter=1000, epsilon=0, kkconst=None, seed=None, " + "minx=None, maxx=None, miny=None, maxy=None, minz=None, maxz=None, dim=2)\n--\n\n" "Places the vertices on a plane according to the Kamada-Kawai algorithm.\n\n" "This is a force directed layout, see Kamada, T. and Kawai, S.:\n" "An Algorithm for Drawing General Undirected Graphs.\n" @@ -14530,10 +14521,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { //////////////////////////// {"bfs", (PyCFunction) igraphmodule_Graph_bfs, METH_VARARGS | METH_KEYWORDS, - "bfs(vid, mode=OUT)\n--\n\n" + "bfs(vid, mode=\"out\")\n--\n\n" "Conducts a breadth first search (BFS) on the graph.\n\n" "@param vid: the root vertex ID\n" - "@param mode: either L{IN} or L{OUT} or L{ALL}, ignored\n" + "@param mode: either C{\"in\"} or C{\"out\"} or C{\"all\"}, ignored\n" " for undirected graphs.\n" "@return: a tuple with the following items:\n" " - The vertex IDs visited (in order)\n" @@ -14541,10 +14532,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " - The parent of every vertex in the BFS\n"}, {"bfsiter", (PyCFunction) igraphmodule_Graph_bfsiter, METH_VARARGS | METH_KEYWORDS, - "bfsiter(vid, mode=OUT, advanced=False)\n--\n\n" + "bfsiter(vid, mode=\"out\", advanced=False)\n--\n\n" "Constructs a breadth first search (BFS) iterator of the graph.\n\n" "@param vid: the root vertex ID\n" - "@param mode: either L{IN} or L{OUT} or L{ALL}.\n" + "@param mode: either C{\"in\"} or C{\"out\"} or C{\"all\"}.\n" "@param advanced: if C{False}, the iterator returns the next\n" " vertex in BFS order in every step. If C{True}, the iterator\n" " returns the distance of the vertex from the root and the\n" @@ -14552,10 +14543,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: the BFS iterator as an L{igraph.BFSIter} object.\n"}, {"dfsiter", (PyCFunction) igraphmodule_Graph_dfsiter, METH_VARARGS | METH_KEYWORDS, - "dfsiter(vid, mode=OUT, advanced=False)\n--\n\n" + "dfsiter(vid, mode=\"out\", advanced=False)\n--\n\n" "Constructs a depth first search (DFS) iterator of the graph.\n\n" "@param vid: the root vertex ID\n" - "@param mode: either L{IN} or L{OUT} or L{ALL}.\n" + "@param mode: either C{\"in\"} or C{\"out\"} or C{\"all\"}.\n" "@param advanced: if C{False}, the iterator returns the next\n" " vertex in DFS order in every step. If C{True}, the iterator\n" " returns the distance of the vertex from the root and the\n" @@ -14569,12 +14560,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { // interface to igraph_get_adjacency {"get_adjacency", (PyCFunction) igraphmodule_Graph_get_adjacency, METH_VARARGS | METH_KEYWORDS, - "get_adjacency(type=GET_ADJACENCY_BOTH, eids=False)\n--\n\n" + "get_adjacency(type=\"both\", eids=False)\n--\n\n" "Returns the adjacency matrix of a graph.\n\n" - "@param type: either C{GET_ADJACENCY_LOWER} (uses the\n" - " lower triangle of the matrix) or C{GET_ADJACENCY_UPPER}\n" - " (uses the upper triangle) or C{GET_ADJACENCY_BOTH}\n" - " (uses both parts). Ignored for directed graphs.\n" + "@param type: one of C{\"lower\"} (uses the lower triangle of the matrix),\n" + " C{\"upper\"} (uses the upper triangle) or C{\"both\"} (uses both parts).\n" + " Ignored for directed graphs.\n" "@param eids: if C{True}, the result matrix will contain\n" " zeros for non-edges and the ID of the edge plus one\n" " for edges in the appropriate cell. If C{False}, the\n" @@ -14616,7 +14606,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " creates one undirected edge for each mutual directed edge pair.\n" "@param combine_edges: specifies how to combine the attributes of\n" " multiple edges between the same pair of vertices into a single\n" - " attribute. See L{Graph.simplify()} for more details.\n" + " attribute. See L{simplify()} for more details.\n" }, /* interface to igraph_laplacian */ @@ -14874,7 +14864,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "canonical_permutation(sh=\"fl\", color=None)\n--\n\n" "Calculates the canonical permutation of a graph using the BLISS isomorphism\n" "algorithm.\n\n" - "Passing the permutation returned here to L{Graph.permute_vertices()} will\n" + "Passing the permutation returned here to L{permute_vertices()} will\n" "transform the graph into its canonical form.\n\n" "See U{http://www.tcs.hut.fi/Software/bliss/index.html} for more information\n" "about the BLISS algorithm and canonical permutations.\n\n" @@ -14918,9 +14908,9 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " - If the graphs have three or four vertices, then an O(1) algorithm\n" " is used with precomputed data.\n\n" " - Otherwise if the graphs are directed, then the VF2 isomorphism\n" - " algorithm is used (see L{Graph.isomorphic_vf2}).\n\n" + " algorithm is used (see L{isomorphic_vf2}).\n\n" " - Otherwise the BLISS isomorphism algorithm is used, see\n" - " L{Graph.isomorphic_bliss}.\n\n" + " L{isomorphic_bliss}.\n\n" "@return: C{True} if the graphs are isomorphic, C{False} otherwise.\n" }, {"isomorphic_bliss", (PyCFunction) igraphmodule_Graph_isomorphic_bliss, @@ -15296,10 +15286,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /**********************/ {"dominator", (PyCFunction) igraphmodule_Graph_dominator, METH_VARARGS | METH_KEYWORDS, - "dominator(vid, mode=)\n--\n\n" + "dominator(vid, mode=\"out\")\n--\n\n" "Returns the dominator tree from the given root node" "@param vid: the root vertex ID\n" - "@param mode: either L{IN} or L{OUT}\n" + "@param mode: either C{\"in\"} or C{\"out\"}\n" "@return: a list containing the dominator tree for the current graph." }, @@ -15639,15 +15629,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { }, {"coreness", (PyCFunction) igraphmodule_Graph_coreness, METH_VARARGS | METH_KEYWORDS, - "coreness(mode=ALL)\n--\n\n" + "coreness(mode=\"all\")\n--\n\n" "Finds the coreness (shell index) of the vertices of the network.\n\n" "The M{k}-core of a graph is a maximal subgraph in which each vertex\n" "has at least degree k. (Degree here means the degree in the\n" "subgraph of course). The coreness of a vertex is M{k} if it\n" "is a member of the M{k}-core but not a member of the M{k+1}-core.\n\n" - "@param mode: whether to compute the in-corenesses (L{IN}), the\n" - " out-corenesses (L{OUT}) or the undirected corenesses (L{ALL}).\n" - " Ignored and assumed to be L{ALL} for undirected graphs.\n" + "@param mode: whether to compute the in-corenesses (C{\"in\"}), the\n" + " out-corenesses (C{\"out\"}) or the undirected corenesses (C{\"all\"}).\n" + " Ignored and assumed to be C{\"all\"} for undirected graphs.\n" "@return: the corenesses for each vertex.\n\n" "@newfield ref: Reference\n" "@ref: Vladimir Batagelj, Matjaz Zaversnik: I{An M{O(m)} Algorithm\n" @@ -15835,7 +15825,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "community_spinglass(weights=None, spins=25, parupdate=False, " "start_temp=1, stop_temp=0.01, cool_fact=0.99, update_rule=\"config\", " - "gamma=1, implementation=\"orig\", lambda=1)\n--\n\n" + "gamma=1, implementation=\"orig\", lambda_=1)\n--\n\n" "Finds the community structure of the graph according to the spinglass\n" "community detection method of Reichardt & Bornholdt.\n\n" "@param weights: edge weights to be used. Can be a sequence or iterable or\n" @@ -15861,7 +15851,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " implementation is the default. The other implementation is able to take\n" " into account negative weights, this can be chosen by setting\n" " C{implementation} to C{\"neg\"}.\n" - "@param lambda: the lambda argument of the algorithm, which specifies the\n" + "@param lambda_: the lambda argument of the algorithm, which specifies the\n" " balance between the importance of present and missing negatively\n" " weighted edges within a community. Smaller values of lambda lead\n" " to communities with less negative intra-connectivity. If the argument\n" @@ -15888,7 +15878,6 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " will be divided by the sum of the node weights. If this is not\n" " supplied, it will default to the node degree, or weighted degree\n" " in case edge_weights are supplied.\n" - "@param node_weights: the node weights used in the Leiden algorithm.\n" "@param beta: parameter affecting the randomness in the Leiden \n" " algorithm. This affects only the refinement step of the algorithm.\n" "@param initial_membership: if provided, the Leiden algorithm\n" @@ -15931,13 +15920,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { METH_VARARGS | METH_KEYWORDS, "_is_maximal_matching(matching, types=None)\n--\n\n" "Internal function, undocumented.\n\n" - "Use L{Matching.is_maximal} instead.\n" + "Use L{igraph.Matching.is_maximal} instead.\n" }, {"_maximum_bipartite_matching", (PyCFunction)igraphmodule_Graph_maximum_bipartite_matching, METH_VARARGS | METH_KEYWORDS, "_maximum_bipartite_matching(types, weights=None)\n--\n\n" "Internal function, undocumented.\n\n" - "@see: L{Graph.maximum_bipartite_matching}\n" + "@see: L{igraph.Graph.maximum_bipartite_matching}\n" }, /****************/ @@ -15949,8 +15938,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Performs a random walk of a given length from a given node.\n\n" "@param start: the starting vertex of the walk\n" "@param steps: the number of steps that the random walk should take\n" - "@param mode: whether to follow outbound edges only (L{OUT}),\n" - " inbound edges only (L{IN}) or both (L{ALL}). Ignored for undirected\n" + "@param mode: whether to follow outbound edges only (C{\"out\"}),\n" + " inbound edges only (C{\"in\"}) or both (C{\"all\"}). Ignored for undirected\n" " graphs." "@param stuck: what to do when the random walk gets stuck. C{\"return\"}\n" " returns a partial random walk; C{\"error\"} throws an exception.\n" diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 0e5f28b77..9d8f485e4 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -715,16 +715,16 @@ GRAPH_PROXY_METHOD_PP(successors, "successors", _convert_to_vertex_list); #define GRAPH_PROXY_METHOD_SPEC(FUNC, METHODNAME) \ {METHODNAME, (PyCFunction)igraphmodule_Vertex_##FUNC, METH_VARARGS | METH_KEYWORDS, \ - "Proxy method to L{Graph." METHODNAME "()}\n\n" \ - "This method calls the " METHODNAME " method of the L{Graph} class " \ + "Proxy method to L{Graph." METHODNAME "()}\n\n" \ + "This method calls the C{" METHODNAME "()} method of the L{Graph} class " \ "with this vertex as the first argument, and returns the result.\n\n"\ - "@see: Graph." METHODNAME "() for details."} + "@see: L{Graph." METHODNAME "()} for details."} #define GRAPH_PROXY_METHOD_SPEC_2(FUNC, METHODNAME, METHODNAME_IN_GRAPH) \ {METHODNAME, (PyCFunction)igraphmodule_Vertex_##FUNC, METH_VARARGS | METH_KEYWORDS, \ - "Proxy method to L{Graph." METHODNAME_IN_GRAPH "()}\n\n" \ - "This method calls the " METHODNAME_IN_GRAPH " method of the L{Graph} class " \ + "Proxy method to L{Graph." METHODNAME_IN_GRAPH "()}\n\n" \ + "This method calls the C{" METHODNAME_IN_GRAPH "} method of the L{Graph} class " \ "with this vertex as the first argument, and returns the result.\n\n"\ - "@see: Graph." METHODNAME_IN_GRAPH "() for details."} + "@see: L{Graph." METHODNAME_IN_GRAPH "()} for details."} /** * \ingroup python_interface_vertex @@ -752,23 +752,23 @@ PyMethodDef igraphmodule_Vertex_methods[] = { "dictionaries." }, {"all_edges", (PyCFunction)igraphmodule_Vertex_all_edges, METH_NOARGS, - "Proxy method to L{Graph.incident(..., mode=\"all\")}\n\n" \ + "Proxy method to L{Graph.incident(..., mode=\"all\")}\n\n" \ "This method calls the incident() method of the L{Graph} class " \ "with this vertex as the first argument and \"all\" as the mode " \ "argument, and returns the result.\n\n"\ - "@see: Graph.incident() for details."}, + "@see: L{Graph.incident()} for details."}, {"in_edges", (PyCFunction)igraphmodule_Vertex_in_edges, METH_NOARGS, - "Proxy method to L{Graph.incident(..., mode=\"in\")}\n\n" \ + "Proxy method to L{Graph.incident(..., mode=\"in\")}\n\n" \ "This method calls the incident() method of the L{Graph} class " \ "with this vertex as the first argument and \"in\" as the mode " \ "argument, and returns the result.\n\n"\ - "@see: Graph.incident() for details."}, + "@see: L{Graph.incident()} for details."}, {"out_edges", (PyCFunction)igraphmodule_Vertex_out_edges, METH_NOARGS, - "Proxy method to L{Graph.incident(..., mode=\"out\")}\n\n" \ + "Proxy method to L{Graph.incident(..., mode=\"out\")}\n\n" \ "This method calls the incident() method of the L{Graph} class " \ "with this vertex as the first argument and \"out\" as the mode " \ "argument, and returns the result.\n\n"\ - "@see: Graph.incident() for details."}, + "@see: L{Graph.incident()} for details."}, GRAPH_PROXY_METHOD_SPEC(betweenness, "betweenness"), GRAPH_PROXY_METHOD_SPEC(closeness, "closeness"), GRAPH_PROXY_METHOD_SPEC(constraint, "constraint"), diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 2101b22d3..77be5ce69 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -994,7 +994,7 @@ PyGetSetDef igraphmodule_VertexSeq_getseters[] = { PyTypeObject igraphmodule_VertexSeqType = { PyVarObject_HEAD_INIT(0, 0) - "igraph.core.VertexSeq", /* tp_name */ + "igraph._igraph.VertexSeq", /* tp_name */ sizeof(igraphmodule_VertexSeqObject), /* tp_basicsize */ 0, /* tp_itemsize */ (destructor)igraphmodule_VertexSeq_dealloc, /* tp_dealloc */ diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 37a45534c..946dd781d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -44,7 +44,7 @@ BLISS_FSM, DFSIter, Edge, - EdgeSeq, + EdgeSeq as _EdgeSeq, GET_ADJACENCY_BOTH, GET_ADJACENCY_LOWER, GET_ADJACENCY_UPPER, @@ -65,7 +65,7 @@ TREE_OUT, TREE_UNDIRECTED, Vertex, - VertexSeq, + VertexSeq as _VertexSeq, WEAK, arpack_options, community_to_membership, @@ -452,7 +452,7 @@ def adjacent(self, *args, **kwds): Returns the edges a given vertex is incident on. - @deprecated: replaced by L{Graph.incident()} since igraph 0.6 + @deprecated: replaced by L{incident()} since igraph 0.6 """ deprecated( "Graph.adjacent() is deprecated since igraph 0.6, please use " @@ -464,7 +464,7 @@ def as_directed(self, *args, **kwds): """as_directed(*args, **kwds) Returns a directed copy of this graph. Arguments are passed on - to L{Graph.to_directed()} that is invoked on the copy. + to L{to_directed()} that is invoked on the copy. """ copy = self.copy() copy.to_directed(*args, **kwds) @@ -474,7 +474,7 @@ def as_undirected(self, *args, **kwds): """as_undirected(*args, **kwds) Returns an undirected copy of this graph. Arguments are passed on - to L{Graph.to_undirected()} that is invoked on the copy. + to L{to_undirected()} that is invoked on the copy. """ copy = self.copy() copy.to_undirected(*args, **kwds) @@ -498,8 +498,8 @@ def delete_edges(self, *args, **kwds): source-target vertex pairs. When multiple edges are present between a given source-target vertex pair, only one is removed. - @deprecated: L{Graph.delete_edges(None)} has been replaced by - L{Graph.delete_edges()} - with no arguments - since igraph 0.8.3. + @deprecated: C{delete_edges(None)} has been replaced by + C{delete_edges()} - with no arguments - since igraph 0.8.3. """ if len(args) == 0 and len(kwds) == 0: return GraphBase.delete_edges(self) @@ -607,7 +607,7 @@ def clear(self): Clears the graph, deleting all vertices, edges, and attributes. - @see: L{Graph.delete_vertices} and L{Graph.delete_edges}. + @see: L{delete_vertices} and L{delete_edges}. """ self.delete_vertices() for attr in self.attributes(): @@ -782,28 +782,24 @@ def get_adjacency_sparse(self, attribute=None): mtx = mtx + sparse.triu(mtx, 1).T + sparse.tril(mtx, -1).T return mtx - def get_adjlist(self, mode=OUT): - """get_adjlist(mode=OUT) - - Returns the adjacency list representation of the graph. + def get_adjlist(self, mode="out"): + """Returns the adjacency list representation of the graph. The adjacency list representation is a list of lists. Each item of the outer list belongs to a single vertex of the graph. The inner list contains the neighbors of the given vertex. - @param mode: if L{OUT}, returns the successors of the vertex. If - L{IN}, returns the predecessors of the vertex. If L{ALL}, both + @param mode: if C{\"out\"}, returns the successors of the vertex. If + C{\"in\"}, returns the predecessors of the vertex. If C{\"all"\"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. """ return [self.neighbors(idx, mode) for idx in range(self.vcount())] def get_adjedgelist(self, *args, **kwds): - """get_adjedgelist(mode=OUT) - - Returns the incidence list representation of the graph. + """Returns the incidence list representation of the graph. - @deprecated: replaced by L{Graph.get_inclist()} since igraph 0.6 + @deprecated: replaced by L{get_inclist()} since igraph 0.6 @see: Graph.get_inclist() """ deprecated( @@ -832,11 +828,11 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode=OUT): means all the vertices. @param cutoff: maximum length of path that is considered. If negative, paths of all lengths are considered. - @param mode: the directionality of the paths. L{IN} means to calculate - incoming paths, L{OUT} means to calculate outgoing paths, L{ALL} means + @param mode: the directionality of the paths. C{\"in\"} means to calculate + incoming paths, C{\"out\"} means to calculate outgoing paths, C{\"all\"} means to calculate both ones. @return: all of the simple paths from the given node to every other - reachable node in the graph in a list. Note that in case of mode=L{IN}, + reachable node in the graph in a list. Note that in case of mode=C{\"in\"}, the vertices in a path are returned in reversed order! """ paths = self._get_all_simple_paths(v, to, cutoff, mode) @@ -858,8 +854,8 @@ def get_inclist(self, mode=OUT): The inner list contains the IDs of the incident edges of the given vertex. - @param mode: if L{OUT}, returns the successors of the vertex. If - L{IN}, returns the predecessors of the vertex. If L{ALL}, both + @param mode: if C{\"out\"}, returns the successors of the vertex. If + C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both the predecessors and the successors will be returned. Ignored for undirected graphs. """ @@ -1318,10 +1314,7 @@ def community_leading_eigenvector_naive(self, clusters=None, return_merges=False def community_leading_eigenvector( self, clusters=None, weights=None, arpack_options=None ): - """community_leading_eigenvector(clusters=None, weights=None, - arpack_options=None) - - Newman's leading eigenvector method for detecting community structure. + """Newman's leading eigenvector method for detecting community structure. This is the proper implementation of the recursive, divisive algorithm: each split is done by maximizing the modularity regarding the original network. @@ -1630,31 +1623,27 @@ def community_leiden( n_iterations=2, node_weights=None, ): - """community_leiden(objective_function=CPM, weights=None, - resolution_parameter=1.0, beta=0.01, initial_membership=None, - n_iterations=2, node_weights=None) - - Finds the community structure of the graph using the - Leiden algorithm of Traag, van Eck & Waltman. + """Finds the community structure of the graph using the Leiden + algorithm of Traag, van Eck & Waltman. - @keyword objective_function: whether to use the Constant Potts + @param objective_function: whether to use the Constant Potts Model (CPM) or modularity. Must be either C{"CPM"} or C{"modularity"}. - @keyword weights: edge weights to be used. Can be a sequence or + @param weights: edge weights to be used. Can be a sequence or iterable or even an edge attribute name. - @keyword resolution_parameter: the resolution parameter to use. + @param resolution_parameter: the resolution parameter to use. Higher resolutions lead to more smaller communities, while lower resolutions lead to fewer larger communities. - @keyword beta: parameter affecting the randomness in the Leiden + @param beta: parameter affecting the randomness in the Leiden algorithm. This affects only the refinement step of the algorithm. - @keyword initial_membership: if provided, the Leiden algorithm + @param initial_membership: if provided, the Leiden algorithm will try to improve this provided membership. If no argument is provided, the aglorithm simply starts from the singleton partition. - @keyword n_iterations: the number of iterations to iterate the Leiden + @param n_iterations: the number of iterations to iterate the Leiden algorithm. Each iteration may improve the partition further. Using a negative number of iterations will run until a stable iteration is encountered (i.e. the quality was not increased during that iteration). - @keyword node_weights: the node weights used in the Leiden algorithm. + @param node_weights: the node weights used in the Leiden algorithm. If this is not provided, it will be automatically determined on the basis of whether you want to use CPM or modularity. If you do provide this, please make sure that you understand what you are doing. @@ -1695,61 +1684,61 @@ def layout(self, layout=None, *args, **kwds): Registered layout names understood by this method are: - C{auto}, C{automatic}: automatic layout - (see L{Graph.layout_auto}) + (see L{layout_auto}) - - C{bipartite}: bipartite layout (see L{Graph.layout_bipartite}) + - C{bipartite}: bipartite layout (see L{layout_bipartite}) - C{circle}, C{circular}: circular layout - (see L{Graph.layout_circle}) + (see L{layout_circle}) - C{dh}, C{davidson_harel}: Davidson-Harel layout (see - L{Graph.layout_davidson_harel}) + L{layout_davidson_harel}) - - C{drl}: DrL layout for large graphs (see L{Graph.layout_drl}) + - C{drl}: DrL layout for large graphs (see L{layout_drl}) - C{drl_3d}: 3D DrL layout for large graphs - (see L{Graph.layout_drl}) + (see L{layout_drl}) - C{fr}, C{fruchterman_reingold}: Fruchterman-Reingold layout - (see L{Graph.layout_fruchterman_reingold}). + (see L{layout_fruchterman_reingold}). - C{fr_3d}, C{fr3d}, C{fruchterman_reingold_3d}: 3D Fruchterman- - Reingold layout (see L{Graph.layout_fruchterman_reingold}). + Reingold layout (see L{layout_fruchterman_reingold}). - - C{grid}: regular grid layout in 2D (see L{Graph.layout_grid}) + - C{grid}: regular grid layout in 2D (see L{layout_grid}) - - C{grid_3d}: regular grid layout in 3D (see L{Graph.layout_grid_3d}) + - C{grid_3d}: regular grid layout in 3D (see L{layout_grid_3d}) - - C{graphopt}: the graphopt algorithm (see L{Graph.layout_graphopt}) + - C{graphopt}: the graphopt algorithm (see L{layout_graphopt}) - C{kk}, C{kamada_kawai}: Kamada-Kawai layout - (see L{Graph.layout_kamada_kawai}) + (see L{layout_kamada_kawai}) - C{kk_3d}, C{kk3d}, C{kamada_kawai_3d}: 3D Kamada-Kawai layout - (see L{Graph.layout_kamada_kawai}) + (see L{layout_kamada_kawai}) - C{lgl}, C{large}, C{large_graph}: Large Graph Layout - (see L{Graph.layout_lgl}) + (see L{layout_lgl}) - - C{mds}: multidimensional scaling layout (see L{Graph.layout_mds}) + - C{mds}: multidimensional scaling layout (see L{layout_mds}) - - C{random}: random layout (see L{Graph.layout_random}) + - C{random}: random layout (see L{layout_random}) - - C{random_3d}: random 3D layout (see L{Graph.layout_random}) + - C{random_3d}: random 3D layout (see L{layout_random}) - C{rt}, C{tree}, C{reingold_tilford}: Reingold-Tilford tree - layout (see L{Graph.layout_reingold_tilford}) + layout (see L{layout_reingold_tilford}) - C{rt_circular}, C{reingold_tilford_circular}: circular Reingold-Tilford tree layout - (see L{Graph.layout_reingold_tilford_circular}) + (see L{layout_reingold_tilford_circular}) - C{sphere}, C{spherical}, C{circle_3d}, C{circular_3d}: spherical - layout (see L{Graph.layout_circle}) + layout (see L{layout_circle}) - - C{star}: star layout (see L{Graph.layout_star}) + - C{star}: star layout (see L{layout_star}) - - C{sugiyama}: Sugiyama layout (see L{Graph.layout_sugiyama}) + - C{sugiyama}: Sugiyama layout (see L{layout_sugiyama}) @param layout: the layout to use. This can be one of the registered layout names or a callable which returns either a L{Layout} object or @@ -1797,14 +1786,14 @@ def layout_auto(self, *args, **kwds): 3. Otherwise, if the graph is connected and has at most 100 vertices, the Kamada-Kawai layout will be used (see - L{Graph.layout_kamada_kawai()}). + L{layout_kamada_kawai()}). 4. Otherwise, if the graph has at most 1000 vertices, the Fruchterman-Reingold layout will be used (see - L{Graph.layout_fruchterman_reingold()}). + L{layout_fruchterman_reingold()}). 5. If everything else above failed, the DrL layout algorithm - will be used (see L{Graph.layout_drl()}). + will be used (see L{layout_drl()}). All the arguments of this function except C{dim} are passed on to the chosen layout function (in case we have to call some layout @@ -1868,10 +1857,7 @@ def layout_sugiyama( maxiter=100, return_extended_graph=False, ): - """layout_sugiyama(layers=None, weights=None, hgap=1, vgap=1, maxiter=100, - return_extended_graph=False) - - Places the vertices using a layered Sugiyama layout. + """Places the vertices using a layered Sugiyama layout. This is a layered layout that is most suitable for directed acyclic graphs, although it works on undirected or cyclic graphs as well. @@ -1973,18 +1959,18 @@ def to_networkx(self): # Graph: decide on directness and mutliplicity if any(self.is_multiple()): if self.is_directed(): - klass = nx.MultiDiGraph + cls = nx.MultiDiGraph else: - klass = nx.MultiGraph + cls = nx.MultiGraph else: if self.is_directed(): - klass = nx.DiGraph + cls = nx.DiGraph else: - klass = nx.Graph + cls = nx.Graph # Graph attributes kw = {x: self[x] for x in self.attributes()} - g = klass(**kw) + g = cls(**kw) # Nodes and node attributes for i, v in enumerate(self.vs): @@ -1997,7 +1983,7 @@ def to_networkx(self): return g @classmethod - def from_networkx(klass, g): + def from_networkx(cls, g): """Converts the graph from networkx Vertex names will be converted to "_nx_name" attribute and the vertices @@ -2018,7 +2004,7 @@ def from_networkx(klass, g): # NOTE: we do not need a special class for multigraphs, it is taken # care for at the edge level rather than at the graph level. - graph = klass( + graph = cls( n=vcount, directed=g.is_directed(), graph_attrs=gattr, vertex_attrs=vattr ) @@ -2105,7 +2091,7 @@ def to_graph_tool( return g @classmethod - def from_graph_tool(klass, g): + def from_graph_tool(cls, g): """Converts the graph from graph-tool @param g: graph-tool Graph @@ -2117,7 +2103,7 @@ def from_graph_tool(klass, g): vcount = g.num_vertices() # Graph - graph = klass(n=vcount, directed=g.is_directed(), graph_attrs=gattr) + graph = cls(n=vcount, directed=g.is_directed(), graph_attrs=gattr) # Node attributes for key, val in list(g.vertex_properties.items()): @@ -2157,12 +2143,12 @@ def write_adjacency(self, f, sep=" ", eol="\n", *args, **kwds): @classmethod def Read_Adjacency( - klass, f, sep=None, comment_char="#", attribute=None, *args, **kwds + cls, f, sep=None, comment_char="#", attribute=None, *args, **kwds ): - """Constructs a graph based on an adjacency matrix from the given file + """Constructs a graph based on an adjacency matrix from the given file. Additional positional and keyword arguments not mentioned here are - passed intact to L{Graph.Adjacency}. + passed intact to L{Adjacency}. @param f: the name of the file to be read or a file object @param sep: the string that separates the matrix elements in a row. @@ -2190,10 +2176,10 @@ def Read_Adjacency( f.close() if attribute is None: - graph = klass.Adjacency(matrix, *args, **kwds) + graph = cls.Adjacency(matrix, *args, **kwds) else: kwds["attr"] = attribute - graph = klass.Weighted_Adjacency(matrix, *args, **kwds) + graph = cls.Weighted_Adjacency(matrix, *args, **kwds) return graph @@ -2291,7 +2277,7 @@ def Read_DIMACS(cls, f, directed=False): return graph @classmethod - def Read_GraphMLz(cls, f, *params, **kwds): + def Read_GraphMLz(cls, f, directed=True, index=0): """Read_GraphMLz(f, directed=True, index=0) Reads a graph from a zipped GraphML file. @@ -2305,10 +2291,9 @@ def Read_GraphMLz(cls, f, *params, **kwds): from igraph.utils import named_temporary_file with named_temporary_file() as tmpfile: - outf = open(tmpfile, "wb") - copyfileobj(gzip.GzipFile(f, "rb"), outf) - outf.close() - return cls.Read_GraphML(tmpfile) + with open(tmpfile, "wb") as outf: + copyfileobj(gzip.GzipFile(f, "rb"), outf) + return cls.Read_GraphML(tmpfile, directed=directed, index=index) def write_pickle(self, fname=None, version=-1): """Saves the graph in Python pickled format @@ -2364,7 +2349,7 @@ def write_picklez(self, fname=None, version=-1): return result @classmethod - def Read_Pickle(klass, fname=None): + def Read_Pickle(cls, fname=None): """Reads a graph from Python pickled format @param fname: the name of the file, a stream to read from, or @@ -2404,7 +2389,7 @@ def Read_Pickle(klass, fname=None): return result @classmethod - def Read_Picklez(klass, fname, *args, **kwds): + def Read_Picklez(cls, fname, *args, **kwds): """Reads a graph from compressed Python pickled format, uncompressing it on-the-fly. @@ -2424,7 +2409,7 @@ def Read_Picklez(klass, fname, *args, **kwds): return result @classmethod - def Read_Picklez(klass, fname, *args, **kwds): + def Read_Picklez(cls, fname, *args, **kwds): """Reads a graph from compressed Python pickled format, uncompressing it on-the-fly. @@ -2441,8 +2426,8 @@ def Read_Picklez(klass, fname, *args, **kwds): result = pickle.load(gzip.GzipFile(mode="rb", fileobj=fname)) else: result = pickle.load(gzip.open(fname, "rb")) - if not isinstance(result, klass): - raise TypeError("unpickled object is not a %s" % klass.__name__) + if not isinstance(result, cls): + raise TypeError("unpickled object is not a %s" % cls.__name__) return result # pylint: disable-msg=C0301,C0323 @@ -2794,7 +2779,7 @@ def write_svg( tk_window.destroy() @classmethod - def _identify_format(klass, filename): + def _identify_format(cls, filename): """_identify_format(filename) Tries to identify the format of the graph stored in the file with the @@ -2880,7 +2865,7 @@ def _identify_format(klass, filename): return "adjacency" @classmethod - def Read(klass, f, format=None, *args, **kwds): + def Read(cls, f, format=None, *args, **kwds): """Unified reading function for graphs. This method tries to identify the format of the graph given in @@ -2904,14 +2889,14 @@ def Read(klass, f, format=None, *args, **kwds): none was given. """ if format is None: - format = klass._identify_format(f) + format = cls._identify_format(f) try: - reader = klass._format_mapping[format][0] + reader = cls._format_mapping[format][0] except (KeyError, IndexError): raise IOError("unknown file format: %s" % str(format)) if reader is None: raise IOError("no reader method for file format: %s" % str(format)) - reader = getattr(klass, reader) + reader = getattr(cls, reader) return reader(f, *args, **kwds) Load = Read @@ -2978,7 +2963,7 @@ def write(self, f, format=None, *args, **kwds): @classmethod def DictList( - klass, + cls, vertices, edges, directed=False, @@ -3050,7 +3035,7 @@ def create_list_from_indices(l, n): # Construct the edges efk_src, efk_dest = edge_foreign_keys if iterative: - g = klass(n, [], directed, {}, vertex_attrs) + g = cls(n, [], directed, {}, vertex_attrs) for idx, edge_data in enumerate(edges): src_name, dst_name = edge_data[efk_src], edge_data[efk_dest] v1 = vertex_name_map[src_name] @@ -3095,14 +3080,14 @@ def create_list_from_indices(l, n): n = len(vertex_name_map) # Create the graph - return klass(n, edge_list, directed, {}, vertex_attrs, edge_attrs) + return cls(n, edge_list, directed, {}, vertex_attrs, edge_attrs) ##################################################### # Constructor for tuple-like representation of graphs @classmethod def TupleList( - klass, + cls, edges, directed=False, vertex_name_attr="name", @@ -3191,7 +3176,7 @@ def TupleList( n = len(idgen) # Construct the graph - return klass(n, edge_list, directed, {}, vertex_attributes, edge_attributes) + return cls(n, edge_list, directed, {}, vertex_attributes, edge_attributes) ################################# # Constructor for graph formulae @@ -3214,10 +3199,8 @@ def es(self): # Friendlier interface for bipartite methods @classmethod - def Bipartite(klass, types, *args, **kwds): - """Bipartite(types, edges, directed=False) - - Creates a bipartite graph with the given vertex types and edges. + def Bipartite(cls, types, edges, directed=False, *args, **kwds): + """Creates a bipartite graph with the given vertex types and edges. This is similar to the default constructor of the graph, the only difference is that it checks whether all the edges go between the two vertex classes and it assigns the type vector @@ -3242,15 +3225,13 @@ def Bipartite(klass, types, *args, **kwds): @return: the graph with a binary vertex attribute named C{"type"} that stores the vertex classes. """ - result = klass._Bipartite(types, *args, **kwds) + result = cls._Bipartite(types, edges, directed, *args, **kwds) result.vs["type"] = [bool(x) for x in types] return result @classmethod - def Full_Bipartite(klass, *args, **kwds): - """Full_Bipartite(n1, n2, directed=False, mode=ALL) - - Generates a full bipartite graph (directed or undirected, with or + def Full_Bipartite(cls, n1, n2, directed=False, mode="all", *args, **kwds): + """Generates a full bipartite graph (directed or undirected, with or without loops). >>> g = Graph.Full_Bipartite(2, 3) @@ -3262,22 +3243,20 @@ def Full_Bipartite(klass, *args, **kwds): @param n1: the number of vertices of the first kind. @param n2: the number of vertices of the second kind. @param directed: whether tp generate a directed graph. - @param mode: if C{OUT}, then all vertices of the first kind are - connected to the others; C{IN} specifies the opposite direction, - C{ALL} creates mutual edges. Ignored for undirected graphs. + @param mode: if C{"out"}, then all vertices of the first kind are + connected to the others; C{"in"} specifies the opposite direction, + C{"all"} creates mutual edges. Ignored for undirected graphs. @return: the graph with a binary vertex attribute named C{"type"} that stores the vertex classes. """ - result, types = klass._Full_Bipartite(*args, **kwds) + result, types = cls._Full_Bipartite(n1, n2, directed, mode, *args, **kwds) result.vs["type"] = types return result @classmethod - def Random_Bipartite(klass, *args, **kwds): - """Random_Bipartite(n1, n2, p=None, m=None, directed=False, neimode=ALL) - - Generates a random bipartite graph with the given number of vertices and + def Random_Bipartite(cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds): + """Generates a random bipartite graph with the given number of vertices and edges (if m is given), or with the given number of vertices and the given connection probability (if p is given). @@ -3299,15 +3278,17 @@ def Random_Bipartite(klass, *args, **kwds): will always point from type 2 to type 1. This argument is ignored for undirected graphs. """ - result, types = klass._Random_Bipartite(*args, **kwds) + if p is None: + p = -1 + if m is None: + m = -1 + result, types = cls._Random_Bipartite(n1, n2, p, m, directed, neimode, *args, **kwds) result.vs["type"] = types return result @classmethod - def GRG(klass, n, radius, torus=False): - """GRG(n, radius, torus=False, return_coordinates=False) - - Generates a random geometric graph. + def GRG(cls, n, radius, torus=False): + """Generates a random geometric graph. The algorithm drops the vertices randomly on the 2D unit square and connects them if they are closer to each other than the given radius. @@ -3319,16 +3300,14 @@ def GRG(klass, n, radius, torus=False): @param torus: This should be C{True} if we want to use a torus instead of a square. """ - result, xs, ys = klass._GRG(n, radius, torus) + result, xs, ys = cls._GRG(n, radius, torus) result.vs["x"] = xs result.vs["y"] = ys return result @classmethod - def Incidence(klass, *args, **kwds): - """Incidence(matrix, directed=False, mode=ALL, multiple=False, weighted=None) - - Creates a bipartite graph from an incidence matrix. + def Incidence(cls, matrix, directed=False, mode="out", multiple=False, weighted=None, *args, **kwds): + """Creates a bipartite graph from an incidence matrix. Example: @@ -3337,10 +3316,10 @@ def Incidence(klass, *args, **kwds): @param matrix: the incidence matrix. @param directed: whether to create a directed graph. @param mode: defines the direction of edges in the graph. If - C{OUT}, then edges go from vertices of the first kind + C{"out"}, then edges go from vertices of the first kind (corresponding to rows of the matrix) to vertices of the - second kind (the columns of the matrix). If C{IN}, the - opposite direction is used. C{ALL} creates mutual edges. + second kind (the columns of the matrix). If C{"in"}, the + opposite direction is used. C{"all"} creates mutual edges. Ignored for undirected graphs. @param multiple: defines what to do with non-zero entries in the matrix. If C{False}, non-zero entries will create an edge no matter @@ -3360,28 +3339,25 @@ def Incidence(klass, *args, **kwds): @return: the graph with a binary vertex attribute named C{"type"} that stores the vertex classes. """ - weighted = kwds.pop("weighted", False) is_weighted = True if weighted or weighted == "" else False - multiple = kwds.get("multiple", False) if is_weighted and multiple: raise ValueError("arguments weighted and multiple can not co-exist") - result, types = klass._Incidence(*args, **kwds) + result, types = cls._Incidence(matrix, directed, mode, multiple, *args, **kwds) result.vs["type"] = types if is_weighted: weight_attr = "weight" if weighted == True else weighted - mat = args[0] _, rows, columns = result.get_incidence() num_vertices_of_first_kind = len(rows) for edge in result.es: source, target = edge.tuple if source in rows: - edge[weight_attr] = mat[source][target - num_vertices_of_first_kind] + edge[weight_attr] = matrix[source][target - num_vertices_of_first_kind] else: - edge[weight_attr] = mat[target][source - num_vertices_of_first_kind] + edge[weight_attr] = matrix[target][source - num_vertices_of_first_kind] return result @classmethod - def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): + def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): """DataFrame(edges, directed=True, vertices=None) Generates a graph from one or two dataframes. @@ -3504,10 +3480,6 @@ def DataFrame(klass, edges, directed=True, vertices=None, use_vids=False): def get_vertex_dataframe(self): """Export vertices with attributes to pandas.DataFrame - @return: a pandas.DataFrame representing vertices and their attributes. - The index uses vertex IDs, from 0 to N - 1 where N is the number of - vertices. - If you want to use vertex names as index, you can do: >>> from string import ascii_letters @@ -3516,6 +3488,9 @@ def get_vertex_dataframe(self): >>> df = graph.get_vertex_dataframe() >>> df.set_index('name', inplace=True) + @return: a pandas.DataFrame representing vertices and their attributes. + The index uses vertex IDs, from 0 to N - 1 where N is the number of + vertices. """ try: import pandas as pd @@ -3533,14 +3508,6 @@ def get_vertex_dataframe(self): def get_edge_dataframe(self): """Export edges with attributes to pandas.DataFrame - @return: a pandas.DataFrame representing edges and their attributes. - The index uses edge IDs, from 0 to M - 1 where M is the number of - edges. The first two columns of the dataframe represent the IDs of - source and target vertices for each edge. These columns have names - "source" and "target". If your edges have attributes with the same - names, they will be present in the dataframe, but not in the first - two columns. - If you want to use source and target vertex IDs as index, you can do: >>> from string import ascii_letters @@ -3560,6 +3527,13 @@ def get_edge_dataframe(self): >>> df['target'].replace(df_vert['name'], inplace=True) >>> df_vert.set_index('name', inplace=True) # Optional + @return: a pandas.DataFrame representing edges and their attributes. + The index uses edge IDs, from 0 to M - 1 where M is the number of + edges. The first two columns of the dataframe represent the IDs of + source and target vertices for each edge. These columns have names + "source" and "target". If your edges have attributes with the same + names, they will be present in the dataframe, but not in the first + two columns. """ try: import pandas as pd @@ -3667,9 +3641,7 @@ def bipartite_projection_size(self, types="type", *args, **kwds): return super(Graph, self).bipartite_projection_size(types, *args, **kwds) def get_incidence(self, types="type", *args, **kwds): - """get_incidence(self, types="type") - - Returns the incidence matrix of a bipartite graph. The incidence matrix + """Returns the incidence matrix of a bipartite graph. The incidence matrix is an M{n} times M{m} matrix, where M{n} and M{m} are the number of vertices in the two vertex classes. @@ -3689,7 +3661,7 @@ def dfs(self, vid, mode=OUT): """Conducts a depth first search (DFS) on the graph. @param vid: the root vertex ID - @param mode: either L{IN} or L{OUT} or L{ALL}, ignored + @param mode: either C{\"in\"} or C{\"out\"} or C{\"all\"}, ignored for undirected graphs. @return: a tuple with the following items: - The vertex IDs visited (in order) @@ -4014,10 +3986,10 @@ def __plot__(self, context, bbox, palette, *args, **kwds): The default is C{False}. - C{layout}: the layout to be used. If not an instance of - L{Layout}, it will be passed to L{Graph.layout} to calculate + L{Layout}, it will be passed to L{layout} to calculate the layout. Note that if you want a deterministic layout that does not change with every plot, you must either use a - deterministic layout function (like L{Graph.layout_circle}) or + deterministic layout function (like L{layout_circle}) or calculate the layout in advance and pass a L{Layout} object here. - C{margin}: the top, right, bottom, left margins as a 4-tuple. @@ -4303,7 +4275,7 @@ def intersection(self, other, byname="auto"): ############################################################## -class VertexSeq(_igraph.VertexSeq): +class VertexSeq(_VertexSeq): """Class representing a sequence of vertices in the graph. This class is most easily accessed by the C{vs} field of the @@ -4361,7 +4333,7 @@ class VertexSeq(_igraph.VertexSeq): Some methods of the vertex sequences are simply proxy methods to the corresponding methods in the L{Graph} object. One such example is - L{VertexSeq.degree()}: + C{VertexSeq.degree()}: >>> g=Graph.Tree(7, 2) >>> g.vs.degree() @@ -4572,7 +4544,7 @@ def __call__(self, *args, **kwds): ############################################################## -class EdgeSeq(_igraph.EdgeSeq): +class EdgeSeq(_EdgeSeq): """Class representing a sequence of edges in the graph. This class is most easily accessed by the C{es} field of the @@ -4633,7 +4605,7 @@ class EdgeSeq(_igraph.EdgeSeq): Some methods of the edge sequences are simply proxy methods to the corresponding methods in the L{Graph} object. One such example is - L{EdgeSeq.is_multiple()}: + C{EdgeSeq.is_multiple()}: >>> g=Graph(3, [(0,1), (1,0), (1,2)]) >>> g.es.is_multiple() @@ -5090,10 +5062,10 @@ def _add_proxy_methods(): rename_methods[VertexSeq] = {"delete_vertices": "delete"} rename_methods[EdgeSeq] = {"delete_edges": "delete", "subgraph_edges": "subgraph"} - for klass, methods in decorated_methods.items(): + for cls, methods in decorated_methods.items(): for method in methods: - new_method_name = rename_methods[klass].get(method, method) - setattr(klass, new_method_name, _graphmethod(None, method)) + new_method_name = rename_methods[cls].get(method, method) + setattr(cls, new_method_name, _graphmethod(None, method)) setattr( EdgeSeq, diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index fcdf10eb8..6defa2449 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -511,11 +511,11 @@ def main(): ] ) for part in parts: - klass = available_classes.get(part, None) - if klass is None: + cls = available_classes.get(part, None) + if cls is None: print("Warning: unknown shell class `%s'" % part, file=sys.stderr) continue - shell_classes.append(klass) + shell_classes.append(cls) else: shell_classes = [IPythonShell, ClassicPythonShell] import platform diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index f1b57e345..e54dc35ac 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -338,14 +338,15 @@ def cluster_graph(self, combine_vertices=None, combine_edges=None): @param combine_vertices: specifies how to derive the attributes of the vertices in the new graph from the attributes of the old ones. - See L{Graph.contract_vertices()} for more details. + See L{Graph.contract_vertices()} + for more details. @param combine_edges: specifies how to derive the attributes of the edges in the new graph from the attributes of the old ones. See - L{igraph.Graph.simplify()} for more details. If you specify C{False} - here, edges will not be combined, and the number of edges between - the vertices representing the original clusters will be equal to - the number of edges between the members of those clusters in the - original graph. + L{Graph.simplify()} for more details. + If you specify C{False} here, edges will not be combined, and the + number of edges between the vertices representing the original + clusters will be equal to the number of edges between the members of + those clusters in the original graph. @return: the new graph. """ diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 7dd9292df..38bd3150c 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -258,8 +258,9 @@ def to_radial(self, min_angle=100, max_angle=80, min_radius=0.0, max_radius=1.0) top-down ones (that's why the Y coordinate belongs to the radius). It can also be used in conjunction with the Fruchterman-Reingold layout algorithm via its I{miny} and I{maxy} parameters (see - L{Graph.layout_fruchterman_reingold}) to produce radial layouts - where the radius belongs to some property of the vertices. + L{Graph.layout_fruchterman_reingold()}) + to produce radial layouts where the radius belongs to some property of + the vertices. @param min_angle: the angle corresponding to the minimum X value @param max_angle: the angle corresponding to the maximum X value From d6d0d57c8f78587f244a37b69377ada2340aacd9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Mar 2021 15:01:12 +0100 Subject: [PATCH 0201/1681] chore: blackened source --- src/igraph/__init__.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 946dd781d..6b4da6516 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3255,7 +3255,9 @@ def Full_Bipartite(cls, n1, n2, directed=False, mode="all", *args, **kwds): return result @classmethod - def Random_Bipartite(cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds): + def Random_Bipartite( + cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds + ): """Generates a random bipartite graph with the given number of vertices and edges (if m is given), or with the given number of vertices and the given connection probability (if p is given). @@ -3282,7 +3284,9 @@ def Random_Bipartite(cls, n1, n2, p=None, m=None, directed=False, neimode="all", p = -1 if m is None: m = -1 - result, types = cls._Random_Bipartite(n1, n2, p, m, directed, neimode, *args, **kwds) + result, types = cls._Random_Bipartite( + n1, n2, p, m, directed, neimode, *args, **kwds + ) result.vs["type"] = types return result @@ -3306,7 +3310,16 @@ def GRG(cls, n, radius, torus=False): return result @classmethod - def Incidence(cls, matrix, directed=False, mode="out", multiple=False, weighted=None, *args, **kwds): + def Incidence( + cls, + matrix, + directed=False, + mode="out", + multiple=False, + weighted=None, + *args, + **kwds + ): """Creates a bipartite graph from an incidence matrix. Example: @@ -3351,9 +3364,13 @@ def Incidence(cls, matrix, directed=False, mode="out", multiple=False, weighted= for edge in result.es: source, target = edge.tuple if source in rows: - edge[weight_attr] = matrix[source][target - num_vertices_of_first_kind] + edge[weight_attr] = matrix[source][ + target - num_vertices_of_first_kind + ] else: - edge[weight_attr] = matrix[target][source - num_vertices_of_first_kind] + edge[weight_attr] = matrix[target][ + source - num_vertices_of_first_kind + ] return result @classmethod From a4be07e98eec4845f51673f4a26727011ed54a39 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 5 Mar 2021 11:22:30 +1100 Subject: [PATCH 0202/1681] Add matplotlib plotting to VertexClustering --- src/igraph/drawing/graph.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 416ff4dfc..0ab460f45 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -1010,6 +1010,22 @@ def callback_edge_offset(event): # FIXME: deal with unnamed *args + # graph is not necessarily a graph, it can be a VertexClustering + # If so, extract the graph and set the coloring unless specified + # externally + from igraph.clustering import VertexClustering + if isinstance(graph, VertexClustering): + clustering = graph + graph = clustering.graph + + if "vertex_color" not in kwds: + clusters = sorted(set(clustering.membership)) + n_colors = len(clusters) + cmap = mpl.cm.get_cmap('viridis') + colors = [cmap(1.0 * i / n_colors) for i in range(n_colors)] + c = [colors[clusters.index(i)] for i in clustering.membership] + kwds["vertex_color"] = c + # Get layout layout = kwds.get("layout", graph.layout()) if isinstance(layout, str): From 3e70e1b6e6787e974ce5a4c8679c93b555a39917 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 6 Mar 2021 08:20:10 +1100 Subject: [PATCH 0203/1681] mark_groups in matplotlib plot --- src/igraph/drawing/graph.py | 61 ++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 0ab460f45..bdcd876c6 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -983,6 +983,8 @@ def draw(self, graph, *args, **kwds): from matplotlib.patches import FancyArrowPatch from matplotlib.patches import ArrowStyle import numpy as np + from collections import defaultdict + from igraph.clustering import VertexClustering def shrink_vertex(ax, aux, vcoord, vsize_squared): """Shrink edge by vertex size""" @@ -1010,14 +1012,34 @@ def callback_edge_offset(event): # FIXME: deal with unnamed *args - # graph is not necessarily a graph, it can be a VertexClustering - # If so, extract the graph and set the coloring unless specified - # externally - from igraph.clustering import VertexClustering + # graph is not necessarily a graph, it can be a VertexClustering. If so + # extract the graph. The clustering itself can be overridden using + # the "mark_groups" option + clustering = None if isinstance(graph, VertexClustering): clustering = graph graph = clustering.graph + # mark groups: the used data structure is eventually the dict option: + # tuples of vertex indices as the keys, colors as the values. We + # convert other formats into that one here + if 'mark_groups' in kwds: + if isinstance(kwds['mark_groups'], VertexClustering): + if clustering is not None: + raise ValueError( + 'mark_groups cannot override a clustering with another' + ) + else: + clustering = kwds.pop('mark_groups') + else: + try: + mg_iter = iter(kwds['mark_groups']) + except TypeError: + raise TypeError('mark_groups is not in the right format') + kwds['mark_groups'] = dict(mg_iter) + + # If a clustering is set, color vertices and mark groups if requested + if clustering is not None: if "vertex_color" not in kwds: clusters = sorted(set(clustering.membership)) n_colors = len(clusters) @@ -1026,6 +1048,16 @@ def callback_edge_offset(event): c = [colors[clusters.index(i)] for i in clustering.membership] kwds["vertex_color"] = c + # mark_groups if not explicitely marked + if 'mark_groups' not in kwds: + mark_groups = defaultdict(list) + for i, color in enumerate(c): + mark_groups[color].append(i) + + # Invert keys and values + mark_groups = {tuple(v): k for k, v in mark_groups.items()} + kwds['mark_groups'] = mark_groups + # Get layout layout = kwds.get("layout", graph.layout()) if isinstance(layout, str): @@ -1034,6 +1066,27 @@ def callback_edge_offset(event): # Vertex coordinates vcoord = layout.coords + # Mark groups + if 'mark_groups' in kwds: + # FIXME: does this generate a new dep? + from scipy.spatial import ConvexHull + for idx, color in kwds['mark_groups'].items(): + points = [vcoord[i] for i in idx] + ch = ConvexHull(points) + vertices = np.array([vcoord[idx[i]] for i in ch.vertices]) + # 15% expansion + vertices += 0.15 * (vertices - vertices.mean(axis=0)) + + # NOTE: we could include a corner cutting algorithm close to + # the vertices (e.g. Chaikin) for beautification + polygon = mpl.patches.Polygon( + vertices, + facecolor=color, alpha=0.3, + zorder=0, edgecolor=color, + lw=2, + ) + ax.add_artist(polygon) + # Vertex properties nv = graph.vcount() From 7b3625de4041c521d840f40439708a7748e86f5d Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sun, 7 Mar 2021 07:21:53 +1100 Subject: [PATCH 0204/1681] Sort out the mark_group option a little --- src/igraph/drawing/graph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index bdcd876c6..2f7580bd1 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -1030,7 +1030,7 @@ def callback_edge_offset(event): 'mark_groups cannot override a clustering with another' ) else: - clustering = kwds.pop('mark_groups') + clustering = kwds['mark_groups'] else: try: mg_iter = iter(kwds['mark_groups']) @@ -1049,7 +1049,7 @@ def callback_edge_offset(event): kwds["vertex_color"] = c # mark_groups if not explicitely marked - if 'mark_groups' not in kwds: + if ('mark_groups' in kwds) and (kwds['mark_groups'] == clustering): mark_groups = defaultdict(list) for i, color in enumerate(c): mark_groups[color].append(i) From bdddc4941f0064ec6c494df574d33b15b2c1e88b Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sun, 7 Mar 2021 07:24:55 +1100 Subject: [PATCH 0205/1681] Handle scipy import and format black --- src/igraph/drawing/graph.py | 39 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 2f7580bd1..4b0ad08ec 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -1023,40 +1023,40 @@ def callback_edge_offset(event): # mark groups: the used data structure is eventually the dict option: # tuples of vertex indices as the keys, colors as the values. We # convert other formats into that one here - if 'mark_groups' in kwds: - if isinstance(kwds['mark_groups'], VertexClustering): + if "mark_groups" in kwds: + if isinstance(kwds["mark_groups"], VertexClustering): if clustering is not None: raise ValueError( - 'mark_groups cannot override a clustering with another' + "mark_groups cannot override a clustering with another" ) else: - clustering = kwds['mark_groups'] + clustering = kwds["mark_groups"] else: try: - mg_iter = iter(kwds['mark_groups']) + mg_iter = iter(kwds["mark_groups"]) except TypeError: - raise TypeError('mark_groups is not in the right format') - kwds['mark_groups'] = dict(mg_iter) + raise TypeError("mark_groups is not in the right format") + kwds["mark_groups"] = dict(mg_iter) # If a clustering is set, color vertices and mark groups if requested if clustering is not None: if "vertex_color" not in kwds: clusters = sorted(set(clustering.membership)) n_colors = len(clusters) - cmap = mpl.cm.get_cmap('viridis') + cmap = mpl.cm.get_cmap("viridis") colors = [cmap(1.0 * i / n_colors) for i in range(n_colors)] c = [colors[clusters.index(i)] for i in clustering.membership] kwds["vertex_color"] = c # mark_groups if not explicitely marked - if ('mark_groups' in kwds) and (kwds['mark_groups'] == clustering): + if ("mark_groups" in kwds) and (kwds["mark_groups"] == clustering): mark_groups = defaultdict(list) for i, color in enumerate(c): mark_groups[color].append(i) # Invert keys and values mark_groups = {tuple(v): k for k, v in mark_groups.items()} - kwds['mark_groups'] = mark_groups + kwds["mark_groups"] = mark_groups # Get layout layout = kwds.get("layout", graph.layout()) @@ -1067,10 +1067,15 @@ def callback_edge_offset(event): vcoord = layout.coords # Mark groups - if 'mark_groups' in kwds: - # FIXME: does this generate a new dep? - from scipy.spatial import ConvexHull - for idx, color in kwds['mark_groups'].items(): + if "mark_groups" in kwds: + try: + from scipy.spatial import ConvexHull + except ImportError: + raise ImportError( + "You should install scipy package in order to use mark_groups" + ) + + for idx, color in kwds["mark_groups"].items(): points = [vcoord[i] for i in idx] ch = ConvexHull(points) vertices = np.array([vcoord[idx[i]] for i in ch.vertices]) @@ -1081,8 +1086,10 @@ def callback_edge_offset(event): # the vertices (e.g. Chaikin) for beautification polygon = mpl.patches.Polygon( vertices, - facecolor=color, alpha=0.3, - zorder=0, edgecolor=color, + facecolor=color, + alpha=0.3, + zorder=0, + edgecolor=color, lw=2, ) ax.add_artist(polygon) From f87ece8fc737c15e6482e90a9a9873d283b18cbb Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 8 Mar 2021 18:57:33 +1100 Subject: [PATCH 0206/1681] Edit docs and enable mark_groups=True in matplotlib --- src/igraph/__init__.py | 6 ++++++ src/igraph/drawing/graph.py | 34 ++++++++++++++++++---------------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 6b4da6516..901bad892 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -4018,6 +4018,12 @@ def __plot__(self, context, bbox, palette, *args, **kwds): - C{False}: no groups will be highlighted + - C{True}: only valid if the object plotted is a + L{VertexClustering} or L{VertexCover}. The vertex groups in the + clutering or cover will be highlighted such that the i-th + group will be colored by the i-th color from the current + palette. If used when plotting a graph, it will throw an error. + - A dict mapping tuples of vertex indices to color names. The given vertex groups will be highlighted by the given colors. diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 4b0ad08ec..3a41accec 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -984,7 +984,7 @@ def draw(self, graph, *args, **kwds): from matplotlib.patches import ArrowStyle import numpy as np from collections import defaultdict - from igraph.clustering import VertexClustering + from igraph.clustering import VertexClustering, VertexCover def shrink_vertex(ax, aux, vcoord, vsize_squared): """Shrink edge by vertex size""" @@ -1016,7 +1016,7 @@ def callback_edge_offset(event): # extract the graph. The clustering itself can be overridden using # the "mark_groups" option clustering = None - if isinstance(graph, VertexClustering): + if isinstance(graph, (VertexClustering, VertexCover)): clustering = graph graph = clustering.graph @@ -1024,13 +1024,19 @@ def callback_edge_offset(event): # tuples of vertex indices as the keys, colors as the values. We # convert other formats into that one here if "mark_groups" in kwds: - if isinstance(kwds["mark_groups"], VertexClustering): + if kwds["mark_groups"] is False: + del kwds["mark_groups"] + elif (kwds["mark_groups"] is True) and (clustering is not None): + pass + # Deferred import to avoid a cycle in the import graph + elif isinstance(kwds["mark_groups"], (VertexClustering, VertexCover)): if clustering is not None: raise ValueError( "mark_groups cannot override a clustering with another" ) else: clustering = kwds["mark_groups"] + kwds["mark_groups"] = True else: try: mg_iter = iter(kwds["mark_groups"]) @@ -1041,15 +1047,18 @@ def callback_edge_offset(event): # If a clustering is set, color vertices and mark groups if requested if clustering is not None: if "vertex_color" not in kwds: - clusters = sorted(set(clustering.membership)) + membership = clustering.membership + if isinstance(clustering, VertexCover): + membership = [x[0] for x in membership] + clusters = sorted(set(membership)) n_colors = len(clusters) cmap = mpl.cm.get_cmap("viridis") colors = [cmap(1.0 * i / n_colors) for i in range(n_colors)] - c = [colors[clusters.index(i)] for i in clustering.membership] + c = [colors[clusters.index(i)] for i in membership] kwds["vertex_color"] = c # mark_groups if not explicitely marked - if ("mark_groups" in kwds) and (kwds["mark_groups"] == clustering): + if ("mark_groups" in kwds) and (kwds["mark_groups"] is True): mark_groups = defaultdict(list) for i, color in enumerate(c): mark_groups[color].append(i) @@ -1068,22 +1077,15 @@ def callback_edge_offset(event): # Mark groups if "mark_groups" in kwds: - try: - from scipy.spatial import ConvexHull - except ImportError: - raise ImportError( - "You should install scipy package in order to use mark_groups" - ) - for idx, color in kwds["mark_groups"].items(): points = [vcoord[i] for i in idx] - ch = ConvexHull(points) - vertices = np.array([vcoord[idx[i]] for i in ch.vertices]) + vertices = np.asarray(convex_hull(points, coords=True)) # 15% expansion vertices += 0.15 * (vertices - vertices.mean(axis=0)) # NOTE: we could include a corner cutting algorithm close to - # the vertices (e.g. Chaikin) for beautification + # the vertices (e.g. Chaikin) for beautification, or a corner + # radius like it's done in the Cairo interface polygon = mpl.patches.Polygon( vertices, facecolor=color, From 78cce97beeced1ca05feaec0601a5cdcda94d99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Mon, 8 Mar 2021 14:59:14 +0100 Subject: [PATCH 0207/1681] fix up changes requested in review --- src/igraph/drawing/graph.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 3a41accec..49d580646 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -977,13 +977,14 @@ def __init__(self, ax): def draw(self, graph, *args, **kwds): # NOTE: matplotlib has numpy as a dependency, so we can use it in here + from collections import defaultdict import matplotlib as mpl import matplotlib.markers as mmarkers from matplotlib.path import Path from matplotlib.patches import FancyArrowPatch from matplotlib.patches import ArrowStyle import numpy as np - from collections import defaultdict + # Deferred import to avoid a cycle in the import graph from igraph.clustering import VertexClustering, VertexCover def shrink_vertex(ax, aux, vcoord, vsize_squared): @@ -1028,7 +1029,6 @@ def callback_edge_offset(event): del kwds["mark_groups"] elif (kwds["mark_groups"] is True) and (clustering is not None): pass - # Deferred import to avoid a cycle in the import graph elif isinstance(kwds["mark_groups"], (VertexClustering, VertexCover)): if clustering is not None: raise ValueError( @@ -1057,8 +1057,8 @@ def callback_edge_offset(event): c = [colors[clusters.index(i)] for i in membership] kwds["vertex_color"] = c - # mark_groups if not explicitely marked - if ("mark_groups" in kwds) and (kwds["mark_groups"] is True): + # mark_groups if not explicitly marked + if kwds.get("mark_groups") is True: mark_groups = defaultdict(list) for i, color in enumerate(c): mark_groups[color].append(i) From dd49def78581064cca94ebcd58b7bf0384e84151 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Mar 2021 19:00:21 +0100 Subject: [PATCH 0208/1681] doc: added missing python-igraph replacement template in global.rst [ci skip] --- doc/source/include/global.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/include/global.rst b/doc/source/include/global.rst index 361937c76..7a928c465 100644 --- a/doc/source/include/global.rst +++ b/doc/source/include/global.rst @@ -1 +1,3 @@ .. |igraph| replace:: *igraph* +.. |python-igraph| replace:: *python-igraph* + From a92409f1e7eaa5ebf1203e8511ff4fbcb8353f04 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 8 Mar 2021 19:49:18 +0100 Subject: [PATCH 0209/1681] doc: removed unneeded function signatures from many docstrings --- src/_igraph/graphobject.c | 18 ++-- src/igraph/__init__.py | 197 +++++++++++--------------------------- 2 files changed, 64 insertions(+), 151 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 48cda7d44..db09ad2b0 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -11980,15 +11980,17 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"vcount", (PyCFunction) igraphmodule_Graph_vcount, METH_NOARGS, "vcount()\n--\n\n" - "Counts the number of vertices.\n" - "@return: the number of vertices in the graph.\n" "@rtype: integer"}, + "Counts the number of vertices.\n\n" + "@return: the number of vertices in the graph.\n" + "@rtype: integer\n"}, // interface to igraph_ecount {"ecount", (PyCFunction) igraphmodule_Graph_ecount, METH_NOARGS, "ecount()\n--\n\n" - "Counts the number of edges.\n" - "@return: the number of edges in the graph.\n" "@rtype: integer"}, + "Counts the number of edges.\n\n" + "@return: the number of edges in the graph.\n" + "@rtype: integer\n"}, // interface to igraph_is_dag {"is_dag", (PyCFunction) igraphmodule_Graph_is_dag, @@ -12003,7 +12005,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"is_directed", (PyCFunction) igraphmodule_Graph_is_directed, METH_NOARGS, "is_directed()\n--\n\n" - "Checks whether the graph is directed.\n" + "Checks whether the graph is directed.\n\n" "@return: C{True} if it is directed, C{False} otherwise.\n" "@rtype: boolean"}, @@ -15260,11 +15262,11 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"vertex_attributes", (PyCFunction) igraphmodule_Graph_vertex_attributes, METH_NOARGS, "vertex_attributes()\n--\n\n" - "@return: the attribute name list of the graph's vertices\n"}, + "@return: the attribute name list of the vertices of the graph\n"}, {"edge_attributes", (PyCFunction) igraphmodule_Graph_edge_attributes, METH_NOARGS, "edge_attributes()\n--\n\n" - "@return: the attribute name list of the graph's edges\n"}, + "@return: the attribute name list of the edges of the graph\n"}, /////////////// // OPERATORS // @@ -15287,7 +15289,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"dominator", (PyCFunction) igraphmodule_Graph_dominator, METH_VARARGS | METH_KEYWORDS, "dominator(vid, mode=\"out\")\n--\n\n" - "Returns the dominator tree from the given root node" + "Returns the dominator tree from the given root node\n\n" "@param vid: the root vertex ID\n" "@param mode: either C{\"in\"} or C{\"out\"}\n" "@return: a list containing the dominator tree for the current graph." diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 901bad892..ea9f6a20a 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -337,9 +337,7 @@ def __init__(self, *args, **kwds): self.es[key] = value def add_edge(self, source, target, **kwds): - """add_edge(source, target, **kwds) - - Adds a single edge to the graph. + """Adds a single edge to the graph. Keyword arguments (except the source and target arguments) will be assigned to the edge as attributes. @@ -359,9 +357,7 @@ def add_edge(self, source, target, **kwds): return edge def add_edges(self, es, attributes=None): - """add_edges(es, attributes=None) - - Adds some edges to the graph. + """Adds some edges to the graph. @param es: the list of edges to be added. Every edge is represented with a tuple containing the vertex IDs or names of the two @@ -379,9 +375,7 @@ def add_edges(self, es, attributes=None): return res def add_vertex(self, name=None, **kwds): - """add_vertex(name=None, **kwds) - - Adds a single vertex to the graph. Keyword arguments will be assigned + """Adds a single vertex to the graph. Keyword arguments will be assigned as vertex attributes. Note that C{name} as a keyword argument is treated specially; if a graph has C{name} as a vertex attribute, it allows one to refer to vertices by their names in most places where igraph expects @@ -401,9 +395,7 @@ def add_vertex(self, name=None, **kwds): return vertex def add_vertices(self, n, attributes=None): - """add_vertices(n, attributes=None) - - Adds some vertices to the graph. + """Adds some vertices to the graph. Note that if C{n} is a sequence of strings, indicating the names of the new vertices, and attributes has a key C{name}, the two conflict. In @@ -447,23 +439,8 @@ def add_vertices(self, n, attributes=None): self.vs[m:][key] = val return result - def adjacent(self, *args, **kwds): - """adjacent(vertex, mode=OUT) - - Returns the edges a given vertex is incident on. - - @deprecated: replaced by L{incident()} since igraph 0.6 - """ - deprecated( - "Graph.adjacent() is deprecated since igraph 0.6, please use " - "Graph.incident() instead" - ) - return self.incident(*args, **kwds) - def as_directed(self, *args, **kwds): - """as_directed(*args, **kwds) - - Returns a directed copy of this graph. Arguments are passed on + """Returns a directed copy of this graph. Arguments are passed on to L{to_directed()} that is invoked on the copy. """ copy = self.copy() @@ -471,9 +448,7 @@ def as_directed(self, *args, **kwds): return copy def as_undirected(self, *args, **kwds): - """as_undirected(*args, **kwds) - - Returns an undirected copy of this graph. Arguments are passed on + """Returns an undirected copy of this graph. Arguments are passed on to L{to_undirected()} that is invoked on the copy. """ copy = self.copy() @@ -603,9 +578,7 @@ def biconnected_components(self, return_articulation_points=False): blocks = biconnected_components def clear(self): - """clear() - - Clears the graph, deleting all vertices, edges, and attributes. + """Clears the graph, deleting all vertices, edges, and attributes. @see: L{delete_vertices} and L{delete_edges}. """ @@ -614,9 +587,7 @@ def clear(self): del self[attr] def cohesive_blocks(self): - """cohesive_blocks() - - Calculates the cohesive block structure of the graph. + """Calculates the cohesive block structure of the graph. Cohesive blocking is a method of determining hierarchical subsets of graph vertices based on their structural cohesion (i.e. vertex connectivity). @@ -633,23 +604,19 @@ def cohesive_blocks(self): """ return CohesiveBlocks(self, *GraphBase.cohesive_blocks(self)) - def clusters(self, mode=STRONG): - """clusters(mode=STRONG) - - Calculates the (strong or weak) clusters (connected components) for + def clusters(self, mode="strong"): + """Calculates the (strong or weak) clusters (connected components) for a given graph. - @param mode: must be either C{STRONG} or C{WEAK}, depending on the - clusters being sought. Optional, defaults to C{STRONG}. + @param mode: must be either C{"strong"} or C{"weak"}, depending on the + clusters being sought. Optional, defaults to C{"strong"}. @return: a L{VertexClustering} object""" return VertexClustering(self, GraphBase.clusters(self, mode)) components = clusters def degree_distribution(self, bin_width=1, *args, **kwds): - """degree_distribution(bin_width=1, ...) - - Calculates the degree distribution of the graph. + """Calculates the degree distribution of the graph. Unknown keyword arguments are directly passed to L{degree()}. @@ -661,9 +628,7 @@ def degree_distribution(self, bin_width=1, *args, **kwds): return result def dyad_census(self, *args, **kwds): - """dyad_census() - - Calculates the dyad census of the graph. + """Calculates the dyad census of the graph. Dyad census means classifying each pair of vertices of a directed graph into three categories: mutual (there is an edge from I{a} to @@ -796,22 +761,8 @@ def get_adjlist(self, mode="out"): """ return [self.neighbors(idx, mode) for idx in range(self.vcount())] - def get_adjedgelist(self, *args, **kwds): - """Returns the incidence list representation of the graph. - - @deprecated: replaced by L{get_inclist()} since igraph 0.6 - @see: Graph.get_inclist() - """ - deprecated( - "Graph.get_adjedgelist() is deprecated since igraph 0.6, " - "please use Graph.get_inclist() instead" - ) - return self.get_inclist(*args, **kwds) - - def get_all_simple_paths(self, v, to=None, cutoff=-1, mode=OUT): - """get_all_simple_paths(v, to=None, cutoff=-1, mode=OUT) - - Calculates all the simple paths from a given node to some other nodes + def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): + """Calculates all the simple paths from a given node to some other nodes (or all of them) in a graph. A path is simple if its vertices are unique, i.e. no vertex is visited @@ -844,10 +795,8 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode=OUT): prev = index + 1 return result - def get_inclist(self, mode=OUT): - """get_inclist(mode=OUT) - - Returns the incidence list representation of the graph. + def get_inclist(self, mode="out"): + """Returns the incidence list representation of the graph. The incidence list representation is a list of lists. Each item of the outer list belongs to a single vertex of the graph. @@ -862,9 +811,7 @@ def get_inclist(self, mode=OUT): return [self.incident(idx, mode) for idx in range(self.vcount())] def gomory_hu_tree(self, capacity=None, flow="flow"): - """gomory_hu_tree(capacity=None, flow="flow") - - Calculates the Gomory-Hu tree of an undirected graph with optional + """Calculates the Gomory-Hu tree of an undirected graph with optional edge capacities. The Gomory-Hu tree is a concise representation of the value of all the @@ -887,25 +834,21 @@ def gomory_hu_tree(self, capacity=None, flow="flow"): return graph def is_named(self): - """is_named() + """Returns whether the graph is named. - Returns whether the graph is named, i.e., whether it has a "name" - vertex attribute. + A graph is named if and only if it has a C{"name"} vertex attribute. """ return "name" in self.vertex_attributes() def is_weighted(self): - """is_weighted() + """Returns whether the graph is weighted. - Returns whether the graph is weighted, i.e., whether it has a "weight" - edge attribute. + A graph is weighted if and only if it has a C{"weight"} edge attribute. """ return "weight" in self.edge_attributes() def maxflow(self, source, target, capacity=None): - """maxflow(source, target, capacity=None) - - Returns a maximum flow between the given source and target vertices + """Returns a maximum flow between the given source and target vertices in a graph. A maximum flow from I{source} to I{target} is an assignment of @@ -930,9 +873,7 @@ def maxflow(self, source, target, capacity=None): return Flow(self, *GraphBase.maxflow(self, source, target, capacity)) def mincut(self, source=None, target=None, capacity=None): - """mincut(source=None, target=None, capacity=None) - - Calculates the minimum cut between the given source and target vertices + """Calculates the minimum cut between the given source and target vertices or within the whole graph. The minimum cut is the minimum set of edges that needs to be removed to @@ -958,9 +899,7 @@ def mincut(self, source=None, target=None, capacity=None): return Cut(self, *GraphBase.mincut(self, source, target, capacity)) def st_mincut(self, source, target, capacity=None): - """st_mincut(source, target, capacity=None) - - Calculates the minimum cut between the source and target vertices in a + """Calculates the minimum cut between the source and target vertices in a graph. @param source: the source vertex ID @@ -975,9 +914,7 @@ def st_mincut(self, source, target, capacity=None): return Cut(self, *GraphBase.st_mincut(self, source, target, capacity)) def modularity(self, membership, weights=None): - """modularity(membership, weights=None) - - Calculates the modularity score of the graph with respect to a given + """Calculates the modularity score of the graph with respect to a given clustering. The modularity of a graph w.r.t. some division measures how good the @@ -1012,9 +949,7 @@ def modularity(self, membership, weights=None): return GraphBase.modularity(self, membership, weights) def path_length_hist(self, directed=True): - """path_length_hist(directed=True) - - Returns the path length histogram of the graph + """Returns the path length histogram of the graph @param directed: whether to consider directed paths. Ignored for undirected graphs. @@ -1042,7 +977,7 @@ def pagerank( niter=1000, eps=0.001, ): - """Calculates the Google PageRank values of a graph. + """Calculates the PageRank values of a graph. @param vertices: the indices of the vertices being queried. C{None} means all of the vertices. @@ -1150,9 +1085,7 @@ def transitivity_avglocal_undirected(self, mode="nan", weights=None): return sum(xs) / float(len(xs)) def triad_census(self, *args, **kwds): - """triad_census() - - Calculates the triad census of the graph. + """Calculates the triad census of the graph. @return: a L{TriadCensus} object. @newfield ref: Reference @@ -1277,11 +1210,9 @@ def community_infomap(self, edge_weights=None, vertex_weights=None, trials=10): ) def community_leading_eigenvector_naive(self, clusters=None, return_merges=False): - """community_leading_eigenvector_naive(clusters=None, - return_merges=False) + """Naive implementation of Newman's eigenvector community structure detection. - A naive implementation of Newman's eigenvector community structure - detection. This function splits the network into two components + This function splits the network into two components according to the leading eigenvector of the modularity matrix and then recursively takes the given number of steps by splitting the communities as individual networks. This is not the correct way, @@ -1315,6 +1246,7 @@ def community_leading_eigenvector( self, clusters=None, weights=None, arpack_options=None ): """Newman's leading eigenvector method for detecting community structure. + This is the proper implementation of the recursive, divisive algorithm: each split is done by maximizing the modularity regarding the original network. @@ -1347,10 +1279,9 @@ def community_leading_eigenvector( return VertexClustering(self, membership, modularity=q) def community_label_propagation(self, weights=None, initial=None, fixed=None): - """community_label_propagation(weights=None, initial=None, fixed=None) - - Finds the community structure of the graph according to the label + """Finds the community structure of the graph according to the label propagation method of Raghavan et al. + Initially, each vertex is assigned a different label. After that, each vertex chooses the dominant label in its neighbourhood in each iteration. Ties are broken randomly and the order in which the @@ -1490,38 +1421,34 @@ def community_edge_betweenness(self, clusters=None, directed=True, weights=None) ) def community_spinglass(self, *args, **kwds): - """community_spinglass(weights=None, spins=25, parupdate=False, - start_temp=1, stop_temp=0.01, cool_fact=0.99, update_rule="config", - gamma=1, implementation="orig", lambda_=1) - - Finds the community structure of the graph according to the + """Finds the community structure of the graph according to the spinglass community detection method of Reichardt & Bornholdt. - @keyword weights: edge weights to be used. Can be a sequence or + @param weights: edge weights to be used. Can be a sequence or iterable or even an edge attribute name. - @keyword spins: integer, the number of spins to use. This is the + @param spins: integer, the number of spins to use. This is the upper limit for the number of communities. It is not a problem to supply a (reasonably) big number here, in which case some spin states will be unpopulated. - @keyword parupdate: whether to update the spins of the vertices in + @param parupdate: whether to update the spins of the vertices in parallel (synchronously) or not - @keyword start_temp: the starting temperature - @keyword stop_temp: the stop temperature - @keyword cool_fact: cooling factor for the simulated annealing - @keyword update_rule: specifies the null model of the simulation. + @param start_temp: the starting temperature + @param stop_temp: the stop temperature + @param cool_fact: cooling factor for the simulated annealing + @param update_rule: specifies the null model of the simulation. Possible values are C{"config"} (a random graph with the same vertex degrees as the input graph) or C{"simple"} (a random graph with the same number of edges) - @keyword gamma: the gamma argument of the algorithm, specifying the + @param gamma: the gamma argument of the algorithm, specifying the balance between the importance of present and missing edges within a community. The default value of 1.0 assigns equal importance to both of them. - @keyword implementation: currently igraph contains two implementations + @param implementation: currently igraph contains two implementations of the spinglass community detection algorithm. The faster original implementation is the default. The other implementation is able to take into account negative weights, this can be chosen by setting C{implementation} to C{"neg"} - @keyword lambda_: the lambda argument of the algorithm, which + @param lambda_: the lambda argument of the algorithm, which specifies the balance between the importance of present and missing negatively weighted edges within a community. Smaller values of lambda lead to communities with less negative intra-connectivity. @@ -1834,9 +1761,7 @@ def layout_auto(self, *args, **kwds): return self.layout(algo, *args, **kwds) def layout_grid_fruchterman_reingold(self, *args, **kwds): - """layout_grid_fruchterman_reingold(*args, **kwds) - - Compatibility alias to the Fruchterman-Reingold layout with the grid + """Compatibility alias to the Fruchterman-Reingold layout with the grid option turned on. @see: Graph.layout_fruchterman_reingold() @@ -2245,9 +2170,7 @@ def write_graphmlz(self, f, compresslevel=9): @classmethod def Read_DIMACS(cls, f, directed=False): - """Read_DIMACS(f, directed=False) - - Reads a graph from a file conforming to the DIMACS minimum-cost flow + """Reads a graph from a file conforming to the DIMACS minimum-cost flow file format. For the exact definition of the format, see @@ -2278,9 +2201,7 @@ def Read_DIMACS(cls, f, directed=False): @classmethod def Read_GraphMLz(cls, f, directed=True, index=0): - """Read_GraphMLz(f, directed=True, index=0) - - Reads a graph from a zipped GraphML file. + """Reads a graph from a zipped GraphML file. @param f: the name of the file @param index: if the GraphML file contains multiple graphs, @@ -3375,9 +3296,7 @@ def Incidence( @classmethod def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): - """DataFrame(edges, directed=True, vertices=None) - - Generates a graph from one or two dataframes. + """Generates a graph from one or two dataframes. @param edges: pandas DataFrame containing edges and metadata. The first two columns of this DataFrame contain the source and target vertices @@ -3640,9 +3559,7 @@ def bipartite_projection( return superclass_meth(types, False, probe1, which) def bipartite_projection_size(self, types="type", *args, **kwds): - """bipartite_projection(types="type") - - Calculates the number of vertices and edges in the bipartite + """Calculates the number of vertices and edges in the bipartite projections of this graph according to the specified vertex types. This is useful if you have a bipartite graph and you want to estimate the amount of memory you would need to calculate the projections @@ -4191,9 +4108,7 @@ def summary(self, verbosity=0, width=None, *args, **kwds): return str(GraphSummary(self, verbosity, width, *args, **kwds)) def disjoint_union(self, other): - """disjoint_union(self, other) - - Creates the disjoint union of two (or more) graphs. + """Creates the disjoint union of two (or more) graphs. @param other: graph or list of graphs to be united with the current one. @return: the disjoint union graph @@ -4203,9 +4118,7 @@ def disjoint_union(self, other): return disjoint_union([self] + other) def union(self, other, byname="auto"): - """union(self, other, byname="auto") - - Creates the union of two (or more) graphs. + """Creates the union of two (or more) graphs. @param other: graph or list of graphs to be united with the current one. @param byname: whether to use vertex names instead of ids. See @@ -4217,9 +4130,7 @@ def union(self, other, byname="auto"): return union([self] + other, byname=byname) def intersection(self, other, byname="auto"): - """intersection(self, other, byname="auto") - - Creates the intersection of two (or more) graphs. + """Creates the intersection of two (or more) graphs. @param other: graph or list of graphs to be intersected with the current one. From 450df181ae1d9f911f78f0e583b22af65bbb451c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 10:47:59 +0100 Subject: [PATCH 0210/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 6bb4e76c1..5414f567e 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 6bb4e76c1d0f971363034e23d06b238a423315cf +Subproject commit 5414f567e45564eace90882ba292306b0fa5a117 From 876763809d3dab4c0143338de4aab8ef40ad5bcc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 11:05:07 +0100 Subject: [PATCH 0211/1681] build: GH actions should clone submodules with a larger depth --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db8c52171..42d29fff1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Init C core submodule - run: git submodule update --init + run: git submodule update --init --depth 1000 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From 23053853c7f1a2e728b67988a04859ef7e9569df Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 13:19:28 +0100 Subject: [PATCH 0212/1681] build: try to forcibly unshallow the vendored igraph repo in CI --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 42d29fff1..3938dec11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Init C core submodule - run: git submodule update --init --depth 1000 + run: git submodule update --init && cd vendor/source/igraph && git --unshallow - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From b8837522704bb0508a6800b45a08f25095d2d4d9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 13:24:35 +0100 Subject: [PATCH 0213/1681] build: add missing git command to unshallowing --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3938dec11..e5a4dcec9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Init C core submodule - run: git submodule update --init && cd vendor/source/igraph && git --unshallow + run: git submodule update --init && cd vendor/source/igraph && git fetch --unshallow - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From 555ee9109df2213f1aec42c26efd9bb88d622142 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 14:10:22 +0100 Subject: [PATCH 0214/1681] build: apparently the submodule is already unshallowed --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5a4dcec9..db8c52171 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v1 - name: Init C core submodule - run: git submodule update --init && cd vendor/source/igraph && git fetch --unshallow + run: git submodule update --init - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From 13e0436b5b5dd142bc9b27430da2e2308c94faaa Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 14:28:38 +0100 Subject: [PATCH 0215/1681] fix: deprecated calling plot() without a target argument --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ src/igraph/configuration.py | 8 +++++++- src/igraph/drawing/__init__.py | 25 ++++++++++++++++++------- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55496ca79..ed42ab4a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # igraph Python interface changelog +## [0.9.1] + +### Changed + +* Calling `plot()` without a filename or a target surface is now deprecated. + The original intention was to plot to a temporary file and then open it in + the default image viewer of the platform of the user automatically, but this + has never worked reliably. The feature will be removed in 0.10.0. + +### Fixed + +* Fixed plotting of `VertexClustering` objects on Matplotlib axes. + +* The `IGRAPH_CMAKE_EXTRA_ARGS` environment variable is now applied _after_ the + default CMake arguments when building the C core of igraph from source. This + enables package maintainers to override any of the default arguments we pass + to CMake. + +* Fixed the documentation build by replacing Epydoc with PyDoctor. + +### Miscellaneous + +* Many old code constructs that were used to maintain compatibility with Python + 2.x are removed now that we have dropped support for Python 2.x. + + ## [0.9.0] ### Added @@ -56,6 +82,7 @@ * The core C library is now built with `-fPIC` on Linux to allow linking to the Python interface. + ## 0.8.3 This is the last released version of `python-igraph` without a changelog file. diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 1c507c4f1..f39a6cdd3 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -40,7 +40,13 @@ def get_platform_image_viewer(): - """Returns the path of an image viewer on the given platform""" + """Returns the path of an image viewer on the given platform. + + Deprecated since python-igraph 0.9.1 and will be removed in 0.10.0. + + @deprecated: This function was only used by the now-deprecated C{show()} + method of the Plot class. + """ plat = platform.system() if plat == "Darwin": # Most likely Mac OS X diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 364eef307..74cf7bdc4 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -318,7 +318,16 @@ def save(self, fname=None): self._surface.finish() def show(self): - """Saves the plot to a temporary file and shows it.""" + """Saves the plot to a temporary file and shows it. + + This method is deprecated from python-igraph 0.9.1 and will be removed in + 0.10.0. + + @deprecated: Opening an image viewer with a temporary file never worked + reliably across platforms. + """ + warn("Plot.show() is deprecated from python-igraph 0.9.1", DeprecationWarning) + if not isinstance(self._surface, cairo.ImageSurface): sur = cairo.ImageSurface( cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) @@ -340,7 +349,7 @@ def show(self): # should only happen on unknown platforms. plat = platform.system() raise NotImplementedError( - "showing plots is not implemented " + "on this platform: %s" % plat + "showing plots is not implemented on this platform: %s" % plat ) else: os.system("%s %s" % (imgviewer, tmpfile)) @@ -415,16 +424,18 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @param target: the target where the object should be plotted. It can be one of the following types: - - C{None} -- an appropriate surface will be created and the object will - be plotted there. + - C{string} -- a file with the given name will be created and an + appropriate Cairo surface will be attached to it. The supported image + formats are: PNG, PDF, SVG and PostScript. - C{cairo.Surface} -- the given Cairo surface will be used. This can refer to a PNG image, an arbitrary window, an SVG file, anything that Cairo can handle. - - C{string} -- a file with the given name will be created and an - appropriate Cairo surface will be attached to it. The supported image - formats are: PNG, PDF, SVG and PostScript. + - C{None} -- a temporary file will be created and the object will be + plotted there. igraph will attempt to open an image viewer and show + the temporary file. This feature is deprecated from python-igraph + version 0.9.1 and will be removed in 0.10.0. @param bbox: the bounding box of the plot. It must be a tuple with either two or four integers, or a L{BoundingBox} object. If this is a tuple From 2df5a5800a723c274393708a0565cd6190a64ca4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 14:39:41 +0100 Subject: [PATCH 0216/1681] build: try with a different igraph revision --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 5414f567e..02fd620c4 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 5414f567e45564eace90882ba292306b0fa5a117 +Subproject commit 02fd620c4d419dae3f76c7b79fdfd1cde8a0e007 From f4ddb0086c66324f0e3ea93c16aa2d5d0c355634 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 14:51:53 +0100 Subject: [PATCH 0217/1681] fix: include generated parser sources in Python tarball so we don't need flex and bison when building the wheels --- setup.py | 78 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 15 deletions(-) diff --git a/setup.py b/setup.py index 88bd0d577..b78b6740a 100644 --- a/setup.py +++ b/setup.py @@ -427,23 +427,71 @@ def sdist(self): """ from setuptools.command.sdist import sdist + def is_git_repo(folder): + return os.path.exists(os.path.join(folder, ".git")) + + def cleanup_git_repo(folder): + folder = str(folder) + cwd = os.getcwd() + try: + os.chdir(folder) + if os.path.exists(".git"): + retcode = subprocess.call("git clean -dfx", shell=True) + if retcode: + raise RuntimeError(f"Failed to clean {folder} with git") + finally: + os.chdir(cwd) + class custom_sdist(sdist): def run(self): - # Clean up vendor/source/igraph with git - cwd = os.getcwd() - try: - os.chdir(os.path.join("vendor", "source", "igraph")) - if os.path.exists(".git"): - retcode = subprocess.call("git clean -dfx", shell=True) - if retcode: - print("Failed to clean vendor/source/igraph with git") - print("") - return False - finally: - os.chdir(cwd) - - # Run the original sdist command - sdist.run(self) + igraph_source_repo = os.path.join("vendor", "source", "igraph") + igraph_build_dir = os.path.join("vendor", "build", "igraph") + version_file = os.path.join(igraph_source_repo, "IGRAPH_VERSION") + version = None + + # Check whether the source repo contains an IGRAPH_VERSION file + if not os.path.exists(version_file): + version_header = os.path.join(igraph_build_dir, "include", "igraph_version.h") + if not os.path.exists(version_header): + raise RuntimeError("You need to build the C core of igraph first before generating a source tarball of python-igraph") + + with open(version_header, "r") as fp: + lines = [line.strip() for line in fp if line.startswith("#define IGRAPH_VERSION ")] + if len(lines) == 1: + version = lines[0].split('"')[1] + + else: + with open(version_file, "r") as fp: + version = fp.read().strip().split("\n")[0] + + if not isinstance(version, str) or len(version) < 5: + raise RuntimeError(f"Cannot determine the version number of the C core in {igraph_source_repo}") + + if not is_git_repo(igraph_source_repo): + # python-igraph was extracted from an official tarball so + # there is no need to tweak anything + return sdist.run(self) + else: + # Clean up vendor/source/igraph with git + cleanup_git_repo(igraph_source_repo) + + # Copy the generated parser sources from the build folder + parser_dir = os.path.join(igraph_build_dir, "src", "io", "parsers") + if not os.path.isdir(parser_dir): + raise RuntimeError(f"You need to build the C core of igraph first before generating a source tarball of python-igraph") + shutil.copytree(parser_dir, os.path.join(igraph_source_repo, "src", "io", "parsers")) + + # Add a version file to the tarball + with open(version_file, "w") as fp: + fp.write(version) + + # Run the original sdist command + retval = sdist.run(self) + + # Clean up vendor/source/igraph with git again + cleanup_git_repo(igraph_source_repo) + + return retval return custom_sdist From b1163a9bd3d809c867efe42e5b743cd2dc9a58d4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 15:06:23 +0100 Subject: [PATCH 0218/1681] fix: updated gitignore to ignore any .so files in src/igraph [ci skip] --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 06705accc..a285acca8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ build/ dist/ doc/api/ -igraph/*.so +src/igraph/*.so *.egg-info/ .python-version .eggs/ From 663ca4dc5343bd9faede1fa9fdd6fc740328f8ff Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 15:08:59 +0100 Subject: [PATCH 0219/1681] chore: updated CHANGELOG [ci skip] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed42ab4a1..d15b63b1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ ### Miscellaneous +* Building `python-igraph` from source should not require `flex` and `bison` + any more; sources of the parsers used by the C core are now included in the + Python source tarball. + * Many old code constructs that were used to maintain compatibility with Python 2.x are removed now that we have dropped support for Python 2.x. From 56ffbbb03a83cd7b77fb1dd6fc886bdef7544483 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 15:18:20 +0100 Subject: [PATCH 0220/1681] build: trying to debug CI failure --- .github/workflows/build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index db8c52171..372bf4445 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,10 @@ jobs: steps: - uses: actions/checkout@v1 - name: Init C core submodule - run: git submodule update --init + run: | + git submodule update --init + cat vendor/source/igraph/.git + cat .git/modules/vendor/source/igraph/HEAD - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: From 0c51f01e2a3f34f1bb05327021d8b0de59897b84 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 15:34:30 +0100 Subject: [PATCH 0221/1681] build: use git describe to determine version number from vendor/source/igraph --- setup.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b78b6740a..2110dadc4 100644 --- a/setup.py +++ b/setup.py @@ -449,8 +449,25 @@ def run(self): version_file = os.path.join(igraph_source_repo, "IGRAPH_VERSION") version = None - # Check whether the source repo contains an IGRAPH_VERSION file - if not os.path.exists(version_file): + # Check whether the source repo contains an IGRAPH_VERSION file, + # and extract the version number from that + if os.path.exists(version_file): + with open(version_file, "r") as fp: + version = fp.read().strip().split("\n")[0] + + # If no IGRAPH_VERSION file exists, but we have a git repo, try + # git describe + if not version and is_git_repo(igraph_source_repo): + cwd = os.getcwd() + try: + os.chdir(igraph_source_repo) + version = subprocess.check_output("git describe", shell=True).decode("utf-8").strip() + finally: + os.chdir(cwd) + + # If we still don't have a version number, try to parse it from + # include/igraph_version.h + if not version: version_header = os.path.join(igraph_build_dir, "include", "igraph_version.h") if not os.path.exists(version_header): raise RuntimeError("You need to build the C core of igraph first before generating a source tarball of python-igraph") @@ -460,10 +477,6 @@ def run(self): if len(lines) == 1: version = lines[0].split('"')[1] - else: - with open(version_file, "r") as fp: - version = fp.read().strip().split("\n")[0] - if not isinstance(version, str) or len(version) < 5: raise RuntimeError(f"Cannot determine the version number of the C core in {igraph_source_repo}") From daba48d71f0825e3c3fc6de0ea13c56f0275da4e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 15:51:54 +0100 Subject: [PATCH 0222/1681] ci: allow setup.py to generate the parsers when running in tox --- setup.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 2110dadc4..bcc104dfe 100644 --- a/setup.py +++ b/setup.py @@ -490,9 +490,14 @@ def run(self): # Copy the generated parser sources from the build folder parser_dir = os.path.join(igraph_build_dir, "src", "io", "parsers") - if not os.path.isdir(parser_dir): + if os.path.isdir(parser_dir): + shutil.copytree(parser_dir, os.path.join(igraph_source_repo, "src", "io", "parsers")) + elif os.environ.get("TESTING_IN_TOX"): + # we allow to proceed when running in tox in the CI environment; + # bison and flex will be used to generate the parsers + pass + else: raise RuntimeError(f"You need to build the C core of igraph first before generating a source tarball of python-igraph") - shutil.copytree(parser_dir, os.path.join(igraph_source_repo, "src", "io", "parsers")) # Add a version file to the tarball with open(version_file, "w") as fp: From 9b4185d98e38cfabe4216fd1989bb3fd40306f04 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 16:04:28 +0100 Subject: [PATCH 0223/1681] ci: we now install all deps with Python 3.9 as well --- tox.ini | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tox.ini b/tox.ini index 212abe28b..73f8166ec 100644 --- a/tox.ini +++ b/tox.ini @@ -24,12 +24,6 @@ deps = setenv = TESTING_IN_TOX=1 -[testenv:py39] -# py39 support is still sparse; most of the optional dependencies have no -# wheels for Python 3.9 yet so we install only those where they do -deps = - networkx - [flake8] max-line-length = 80 select = C,E,F,W,B,B950 From dea92d46285af68429ee4278a8d934e734eb5830 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 16:04:52 +0100 Subject: [PATCH 0224/1681] ci: set TESTING_IN_TOX envvar also when building the sdist --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 372bf4445..b84ae4a72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -78,4 +78,6 @@ jobs: pip install tox tox-gh-actions - name: Test with tox run: tox + env: + TESTING_IN_TOX: 1 From 849490bbc475e5391ab02e7303323a13aa75d0af Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 16:12:29 +0100 Subject: [PATCH 0225/1681] ci set TESTING_IN_TOX for both macOS and Linux jobs --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b84ae4a72..a2c207e6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,6 +50,8 @@ jobs: pip install tox tox-gh-actions - name: Test with tox run: tox + env: + TESTING_IN_TOX: 1 build_osx: runs-on: macos-latest From a2a2ee529521c7e30253efd9aa8b3643005e1120 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 17:25:31 +0100 Subject: [PATCH 0226/1681] doc: fix docstring of two tests --- tests/test_cliques.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_cliques.py b/tests/test_cliques.py index f972fd715..70a958376 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -163,10 +163,8 @@ def setUp(self): self.g = Graph.Erdos_Renyi(100, 0.2, directed=True) def testDyads(self): - """ - @note: this test is not exhaustive, it only checks whether the - L{DyadCensus} objects "understand" attribute and item accessors - """ + # @note: this test is not exhaustive, it only checks whether the + # L{DyadCensus} objects "understand" attribute and item accessors dc = self.g.dyad_census() accessors = ["mut", "mutual", "asym", "asymm", "asymmetric", "null"] for a in accessors: @@ -178,10 +176,8 @@ def testDyads(self): self.assertTrue(len(tuple(dc)) == 3) def testTriads(self): - """ - @note: this test is not exhaustive, it only checks whether the - L{TriadCensus} objects "understand" attribute and item accessors - """ + # @note: this test is not exhaustive, it only checks whether the + # L{TriadCensus} objects "understand" attribute and item accessors tc = self.g.triad_census() accessors = ["003", "012", "021d", "030C"] for a in accessors: From ddc53b66c03cdf0ced19836addfa81fc045c1aab Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 17:26:06 +0100 Subject: [PATCH 0227/1681] fix: fix tests for SciPy sparse adjacency matrix constructor --- src/igraph/__init__.py | 9 +- src/igraph/drawing/graph.py | 1 + src/igraph/sparse_matrix.py | 180 ++++++++++++++++++++++-------------- tests/test_generators.py | 24 ++++- 4 files changed, 137 insertions(+), 77 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 8a9367561..9dc0792d2 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2157,7 +2157,7 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): return super().Adjacency(matrix, mode=mode) @classmethod - def WeightedAdjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=True): + def Weighted_Adjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=True): """Generates a graph from its weighted adjacency matrix. @param matrix: the adjacency matrix. Possible types are: @@ -2196,18 +2196,19 @@ def WeightedAdjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=Tru except ImportError: sparse = None - if (sparse is not None) and isinstance(matrix, sparse.spmatrix): + if sparse is not None and isinstance(matrix, sparse.spmatrix): return _graph_from_weighted_sparse_matrix( klass, matrix, mode=mode, attr=attr, + loops=loops, ) - if (np is not None) and isinstance(matrix, np.ndarray): + if np is not None and isinstance(matrix, np.ndarray): matrix = matrix.tolist() - return super().WeightedAdjacency( + return super().Weighted_Adjacency( matrix, mode=mode, attr=attr, diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 49d580646..d46b45538 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -984,6 +984,7 @@ def draw(self, graph, *args, **kwds): from matplotlib.patches import FancyArrowPatch from matplotlib.patches import ArrowStyle import numpy as np + # Deferred import to avoid a cycle in the import graph from igraph.clustering import VertexClustering, VertexCover diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index 4d1d8e1d2..132d68bc4 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -37,81 +37,109 @@ ) +_SUPPORTED_MODES = ("directed", "undirected", "max", "min", "plus", "lower", "upper") + + +def _convert_mode_argument(mode): + # resolve mode constants, convert to lowercase + mode = ( + { + ADJ_DIRECTED: "directed", + ADJ_UNDIRECTED: "undirected", + ADJ_MAX: "max", + ADJ_MIN: "min", + ADJ_PLUS: "plus", + ADJ_UPPER: "upper", + ADJ_LOWER: "lower", + } + .get(mode, mode) + .lower() + ) + + if mode not in _SUPPORTED_MODES: + raise ValueError("mode should be one of " + (" ".join(_SUPPORTED_MODES))) + + if mode == "undirected": + mode = "max" + + return mode + + # Logic to get graph from scipy sparse matrix. This would be simple if there # weren't so many modes. -def _graph_from_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED): +def _graph_from_sparse_matrix(klass, matrix, mode="directed"): """Construct graph from sparse matrix, unweighted""" # This function assumes there is scipy and the matrix is a scipy sparse # matrix. The caller should make sure those conditions are met. from scipy import sparse - modes = ( - ADJ_DIRECTED, - ADJ_UNDIRECTED, - ADJ_MAX, - ADJ_MIN, - ADJ_PLUS, - ADJ_UPPER, - ADJ_LOWER, - ) - if not isinstance(matrix, sparse.coo_matrix): matrix = matrix.tocoo() + nvert = max(matrix.shape) + if min(matrix.shape) != nvert: + raise ValueError("Matrix must be square") + # Shorthand notation m = matrix - if mode == ADJ_UNDIRECTED: - mode = ADJ_MAX + mode = _convert_mode_argument(mode) - if mode == ADJ_DIRECTED: + if mode == "directed": edges = sum( ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data)), [], ) - elif mode in (ADJ_MAX, ADJ_MIN, ADJ_PLUS): - fun_dict = { - ADJ_MAX: max, - ADJ_MIN: min, - ADJ_PLUS: add, - } - fun = fun_dict[mode] + elif mode in ("max", "plus"): + fun = max if mode == "max" else add nedges = {} for i, j, n in zip(m.row, m.col, m.data): - # Fist time this pair of vertices - if (j, i) not in nedges: - nedges[(i, j)] = n - else: - nedges[(j, i)] = fun(nedges[(j, i)], n) + pair = (i, j) if i < j else (j, i) + nedges[pair] = fun(nedges.get(pair, 0), n) + + edges = sum( + ([e] * n for e, n in nedges.items()), + [], + ) + + elif mode == "min": + tmp = {(i, j): n for i, j, n in zip(m.row, m.col, m.data)} + + nedges = {} + for pair, weight in tmp.items(): + i, j = pair + if i == j: + nedges[pair] = weight + elif i < j: + nedges[pair] = min(weight, tmp.get((j, i), 0)) edges = sum( ([e] * n for e, n in nedges.items()), [], ) - elif mode == ADJ_UPPER: + elif mode == "upper": edges = sum( ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j >= i), [], ) - elif mode == ADJ_LOWER: + elif mode == "lower": edges = sum( ([(i, j)] * n for i, j, n in zip(m.row, m.col, m.data) if j <= i), [], ) else: - raise ValueError("mode should be one of " + " ".join(map(str, modes))) + raise ValueError("invalid mode") - return klass( - edges=edges, - directed=mode == ADJ_DIRECTED, - ) + return klass(nvert, edges=edges, directed=(mode == "directed")) -def _graph_from_weighted_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED, attr="weight"): +def _graph_from_weighted_sparse_matrix( + klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=True +): """Construct graph from sparse matrix, weighted NOTE: Of course, you cannot emcompass a fully general weighted multigraph @@ -121,65 +149,75 @@ def _graph_from_weighted_sparse_matrix(klass, matrix, mode=ADJ_DIRECTED, attr="w # matrix. The caller should make sure those conditions are met. from scipy import sparse - modes = ( - ADJ_DIRECTED, - ADJ_UNDIRECTED, - ADJ_MAX, - ADJ_MIN, - ADJ_PLUS, - ADJ_UPPER, - ADJ_LOWER, - ) - if not isinstance(matrix, sparse.coo_matrix): matrix = matrix.tocoo() + nvert = max(matrix.shape) + if min(matrix.shape) != nvert: + raise ValueError("Matrix must be square") + # Shorthand notation m = matrix - if mode == ADJ_UNDIRECTED: - mode = ADJ_MAX - - if mode == ADJ_DIRECTED: - edges = list(zip(m.row, m.col)) - weights = list(m.data) - - elif mode in (ADJ_MAX, ADJ_MIN, ADJ_PLUS): - fun_dict = { - ADJ_MAX: max, - ADJ_MIN: min, - ADJ_PLUS: add, - } - fun = fun_dict[mode] + mode = _convert_mode_argument(mode) + + if mode == "directed": + if not loops: + edges, weights = [], [] + for i, j, n in zip(m.row, m.col, m.data): + if i != j: + edges.append((i, j)) + weights.append(n) + else: + edges = list(zip(m.row, m.col)) + weights = list(m.data) + + elif mode in ("max", "plus"): + fun = max if mode == "max" else add nedges = {} for i, j, n in zip(m.row, m.col, m.data): - # Fist time this pair of vertices - if (j, i) not in nedges: - nedges[(i, j)] = n - else: - nedges[(j, i)] = fun(nedges[(j, i)], n) + if i == j and not loops: + continue + pair = (i, j) if i < j else (j, i) + nedges[pair] = fun(nedges.get(pair, 0), n) edges, weights = zip(*nedges.items()) - elif mode == ADJ_UPPER: + elif mode == "min": + tmp = {(i, j): n for i, j, n in zip(m.row, m.col, m.data)} + + nedges = {} + for pair, weight in tmp.items(): + i, j = pair + if i == j and loops: + nedges[pair] = weight + elif i < j: + nedges[pair] = min(weight, tmp.get((j, i), 0)) + + edges, weights = [], [] + for pair in sorted(nedges.keys()): + weight = nedges[pair] + if weight != 0: + edges.append(pair) + weights.append(nedges[pair]) + + elif mode == "upper": edges, weights = [], [] for i, j, n in zip(m.row, m.col, m.data): - if j >= i: + if j > i or (loops and j == i): edges.append((i, j)) weights.append(n) - elif mode == ADJ_LOWER: + elif mode == "lower": edges, weights = [], [] for i, j, n in zip(m.row, m.col, m.data): - if j <= i: + if j < i or (loops and j == i): edges.append((i, j)) weights.append(n) else: - raise ValueError("mode should be one of " + " ".join(map(str, modes))) + raise ValueError("invalid mode") return klass( - edges=edges, - directed=mode == ADJ_DIRECTED, - edge_attrs={attr: weights}, + nvert, edges=edges, directed=(mode == "directed"), edge_attrs={attr: weights} ) diff --git a/tests/test_generators.py b/tests/test_generators.py index 1e296c3ee..6895cac0a 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -210,16 +210,22 @@ def testSparseAdjacency(self): # ADJ_DIRECTED (default) g = Graph.Adjacency(mat) el = g.get_edgelist() + self.assertTrue(g.is_directed()) + self.assertEqual(4, g.vcount()) self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (2, 2), (3, 1)]) # ADJ MIN g = Graph.Adjacency(mat, mode="min") el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) self.assertTrue(el == [(0, 1), (2, 2), (2, 2)]) # ADJ LOWER g = Graph.Adjacency(mat, mode="lower") el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) self.assertTrue(el == [(0, 1), (2, 2), (2, 2), (1, 3)]) def testWeightedAdjacency(self): @@ -271,16 +277,30 @@ def testSparseWeighedAdjacency(self): g = Graph.Weighted_Adjacency(mat, attr="w0") el = g.get_edgelist() + self.assertTrue(g.is_directed()) + self.assertEqual(4, g.vcount()) self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (2, 2), (3, 1)]) self.assertTrue(g.es["w0"] == [1, 2, 2, 2.5, 1]) g = Graph.Weighted_Adjacency(mat, mode="plus") el = g.get_edgelist() - self.assertTrue(el == [(0, 1), (0, 2), (1, 3), (2, 2)]) - self.assertTrue(g.es["weight"] == [3, 2, 1, 2.5]) + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + self.assertTrue(el == [(0, 1), (0, 2), (2, 2), (1, 3)]) + self.assertTrue(g.es["weight"] == [3, 2, 2.5, 1]) + + g = Graph.Weighted_Adjacency(mat, mode="min") + el = g.get_edgelist() + self.assertFalse(g.is_directed()) + self.assertEqual(4, g.vcount()) + print(repr(el)) + self.assertTrue(el == [(0, 1), (2, 2)]) + self.assertTrue(g.es["weight"] == [1, 2.5]) g = Graph.Weighted_Adjacency(mat, attr="w0", loops=False) el = g.get_edgelist() + self.assertTrue(g.is_directed()) + self.assertEqual(4, g.vcount()) self.assertTrue(el == [(0, 1), (0, 2), (1, 0), (3, 1)]) self.assertTrue(g.es["w0"] == [1, 2, 2, 1]) From e8269f49563fed28b3ddea4af44b91f552517800 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Mar 2021 17:26:19 +0100 Subject: [PATCH 0228/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 02fd620c4..55b9cdca9 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 02fd620c4d419dae3f76c7b79fdfd1cde8a0e007 +Subproject commit 55b9cdca9aa47832f841aa1edaf9c13aa1d46a98 From a337298d31882629463590e2d1d9a6f8354d71c5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 12 Mar 2021 10:13:18 +0100 Subject: [PATCH 0229/1681] fix: re-export __igraph_version__ from igraph._igraph --- src/igraph/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 9dc0792d2..23d674b6d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -76,6 +76,7 @@ set_progress_handler, set_random_number_generator, set_status_handler, + __igraph_version__ ) from igraph.clustering import ( Clustering, From d90cd39110cde57d7ff642a6884d5c295f94b3ed Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Mar 2021 14:09:07 +0100 Subject: [PATCH 0230/1681] fix: remove PyInt_AsInt() from compatibility functions, it is now the same as PyLong_As_int() in Python 3.x --- src/_igraph/attributes.c | 2 +- src/_igraph/convert.c | 55 ++++---------------------------------- src/_igraph/convert.h | 1 - src/_igraph/graphobject.c | 2 +- src/_igraph/vertexobject.c | 4 +-- 5 files changed, 9 insertions(+), 55 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 8a8cf79a5..124b34ed5 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -155,7 +155,7 @@ int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_inte return 1; } - if (PyInt_AsInt(o_vid, &tmp)) + if (PyLong_AsInt(o_vid, &tmp)) return 1; *vid = tmp; diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index f92013947..0fcebcf46 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -39,30 +39,6 @@ #define strcasecmp _stricmp #endif -/** - * \brief Converts a Python integer to a C int - * - * This is similar to PyLong_AsLong, but it checks for overflow first and throws - * an exception if necessary. - * - * Returns -1 if there was an error, 0 otherwise. - */ -int PyInt_AsInt(PyObject* obj, int* result) { - long dummy = PyLong_AsLong(obj); - if (dummy < INT_MIN) { - PyErr_SetString(PyExc_OverflowError, - "integer too small for conversion to C int"); - return -1; - } - if (dummy > INT_MAX) { - PyErr_SetString(PyExc_OverflowError, - "integer too large for conversion to C int"); - return -1; - } - *result = (int)dummy; - return 0; -} - /** * \brief Converts a Python long to a C int * @@ -74,13 +50,11 @@ int PyInt_AsInt(PyObject* obj, int* result) { int PyLong_AsInt(PyObject* obj, int* result) { long dummy = PyLong_AsLong(obj); if (dummy < INT_MIN) { - PyErr_SetString(PyExc_OverflowError, - "long integer too small for conversion to C int"); + PyErr_SetString(PyExc_OverflowError, "long integer too small for conversion to C int"); return -1; } if (dummy > INT_MAX) { - PyErr_SetString(PyExc_OverflowError, - "long integer too large for conversion to C int"); + PyErr_SetString(PyExc_OverflowError, "long integer too large for conversion to C int"); return -1; } *result = (int)dummy; @@ -114,8 +88,6 @@ int igraphmodule_PyObject_to_enum(PyObject *o, if (o == 0 || o == Py_None) return 0; - if (PyLong_Check(o)) - return PyInt_AsInt(o, result); if (PyLong_Check(o)) return PyLong_AsInt(o, result); s = PyUnicode_CopyAsString(o); @@ -725,7 +697,7 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { PyObject *i = PyNumber_Long(object); if (i == NULL) return 1; - retval = PyInt_AsInt(i, &num); + retval = PyLong_AsInt(i, &num); Py_DECREF(i); if (retval) return retval; @@ -1013,7 +985,7 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v PyErr_SetString(PyExc_TypeError, "can't convert a list item to integer"); ok = 0; } else { - ok = (PyInt_AsInt(item, &value) == 0); + ok = (PyLong_AsInt(item, &value) == 0); Py_DECREF(item2); } } @@ -1057,7 +1029,7 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v PyErr_SetString(PyExc_TypeError, "can't convert sequence element to int"); ok=0; } else { - retval = PyInt_AsInt(item2, &value); + retval = PyLong_AsInt(item2, &value); if (retval) ok = 0; Py_DECREF(item2); @@ -2387,11 +2359,6 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g if (o == Py_None || o == 0) { *vid = 0; - } else if (PyLong_Check(o)) { - /* Single vertex ID */ - if (PyInt_AsInt(o, &tmp)) - return 1; - *vid = tmp; } else if (PyLong_Check(o)) { /* Single vertex ID */ if (PyLong_AsInt(o, &tmp)) @@ -2410,13 +2377,6 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g PyObject* num = PyNumber_Index(o); if (num) { if (PyLong_Check(num)) { - retval = PyInt_AsInt(num, &tmp); - if (retval) { - Py_DECREF(num); - return 1; - } - *vid = tmp; - } else if (PyLong_Check(num)) { retval = PyLong_AsInt(num, &tmp); if (retval) { Py_DECREF(num); @@ -2599,11 +2559,6 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g if (o == Py_None || o == 0) { *eid = 0; - } else if (PyLong_Check(o)) { - /* Single edge ID */ - if (PyInt_AsInt(o, &tmp)) - return 1; - *eid = tmp; } else if (PyLong_Check(o)) { /* Single edge ID */ if (PyLong_AsInt(o, &tmp)) diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index d777406d4..8834420cf 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -41,7 +41,6 @@ typedef struct { int value; } igraphmodule_enum_translation_table_entry_t; -int PyInt_AsInt(PyObject* obj, int* result); int PyLong_AsInt(PyObject* obj, int* result); /* Conversion from PyObject to enum types */ diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index fca586393..0ced8a25f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4962,7 +4962,7 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (PyInt_AsInt(cutoff_o, &cutoff)) + if (PyLong_AsInt(cutoff_o, &cutoff)) return NULL; if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 9d8f485e4..287ecba0e 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -616,7 +616,7 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje return NULL; } - if (PyInt_AsInt(idx, &idx_int)) + if (PyLong_AsInt(idx, &idx_int)) return NULL; v = igraphmodule_Edge_New(vertex->gref, idx_int); @@ -648,7 +648,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb return NULL; } - if (PyInt_AsInt(idx, &idx_int)) + if (PyLong_AsInt(idx, &idx_int)) return NULL; v = igraphmodule_Vertex_New(vertex->gref, idx_int); From 26a8bb5e07f6e8d55cd6742ec8a3eb5e6a38950e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Mar 2021 14:10:13 +0100 Subject: [PATCH 0231/1681] fix: removed one more occurrence of PyInt_As_int --- src/_igraph/convert.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 0fcebcf46..cac8132e5 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2573,13 +2573,6 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g PyObject* num = PyNumber_Index(o); if (num) { if (PyLong_Check(num)) { - retval = PyInt_AsInt(num, &tmp); - if (retval) { - Py_DECREF(num); - return 1; - } - *eid = tmp; - } else if (PyLong_Check(num)) { retval = PyLong_AsInt(num, &tmp); if (retval) { Py_DECREF(num); From 6c20f7c7791a8d59a1722b41125a6fae22f0ac4b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Mar 2021 14:34:57 +0100 Subject: [PATCH 0232/1681] fix: updated to igraph 0.9.1, fixed a bug in the handling of the finally stack when converting Python integer tuples to edge sequences --- src/_igraph/convert.c | 103 ++++++++++++++++++++++++++++++------------ tests/test_basic.py | 4 ++ vendor/source/igraph | 2 +- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index cac8132e5..010f83877 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -842,7 +842,11 @@ int igraphmodule_PyObject_to_vector_t(PyObject *list, igraph_vector_t *v, igraph igraph_vector_destroy(v); return 1; } - igraph_vector_push_back(v, number); + if (igraph_vector_push_back(v, number)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(v); + return 1; + } } } @@ -1985,11 +1989,11 @@ PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, for (j=0; jvs)) { igraphmodule_handle_igraph_error(); return 1; } - if (return_single) + + if (return_single) { *return_single = 0; + } + return 0; } @@ -2458,21 +2466,27 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, if (start == 0 && slicelength == no_of_vertices) { igraph_vs_all(vs); } else { - IGRAPH_CHECK(igraph_vector_init(&vector, slicelength)); - IGRAPH_FINALLY(igraph_vector_destroy, &vector); + if (igraph_vector_init(&vector, slicelength)) { + igraphmodule_handle_igraph_error(); + return 1; + } for (i = 0; i < slicelength; i++, start += step) { VECTOR(vector)[i] = start; } - IGRAPH_CHECK(igraph_vs_vector_copy(vs, &vector)); + if (igraph_vs_vector_copy(vs, &vector)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&vector); + return 1; + } igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); } - if (return_single) + if (return_single) { *return_single = 0; + } return 0; } @@ -2500,9 +2514,10 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, return 1; } - IGRAPH_CHECK(igraph_vector_init(&vector, 0)); - IGRAPH_FINALLY(igraph_vector_destroy, &vector); - IGRAPH_CHECK(igraph_vector_reserve(&vector, 20)); + if (igraph_vector_init(&vector, 0)) { + igraphmodule_handle_igraph_error(); + return 1; + } while ((item = PyIter_Next(iterator))) { vid = -1; @@ -2511,22 +2526,31 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, break; Py_DECREF(item); - igraph_vector_push_back(&vector, vid); + + if (igraph_vector_push_back(&vector, vid)) { + igraphmodule_handle_igraph_error(); + /* no need to destroy 'vector' here; will be done outside the loop due + * to PyErr_Occurred */ + break; + } } + Py_DECREF(iterator); if (PyErr_Occurred()) { igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); return 1; } - IGRAPH_CHECK(igraph_vs_vector_copy(vs, &vector)); - igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); + if (igraph_vs_vector_copy(vs, &vector)) { + igraphmodule_handle_igraph_error(); + igraph_vector_destroy(&vector); + return 1; + } - if (return_single) + if (return_single) { *return_single = 0; + } return 0; } @@ -2603,7 +2627,16 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g if (igraphmodule_PyObject_to_vid(o2, &vid2, graph)) return 1; - igraph_get_eid(graph, eid, vid1, vid2, 1, 0); + retval = igraph_get_eid(graph, eid, vid1, vid2, 1, 0); + if (retval == IGRAPH_EINVVID) { + PyErr_Format(PyExc_ValueError, "no edge from vertex #%ld to #%ld; no such vertex ID", + (long int)vid1, (long int)vid2); + return 1; + } else if (retval) { + igraphmodule_handle_igraph_error(); + return 1; + } + if (*eid < 0) { PyErr_Format(PyExc_ValueError, "no edge from vertex #%ld to #%ld", (long int)vid1, (long int)vid2); @@ -2679,24 +2712,32 @@ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, return 1; } - IGRAPH_CHECK(igraph_vector_init(&vector, 0)); - IGRAPH_FINALLY(igraph_vector_destroy, &vector); - IGRAPH_CHECK(igraph_vector_reserve(&vector, 20)); + if (igraph_vector_init(&vector, 0)) { + igraphmodule_handle_igraph_error(); + return 1; + } while ((item = PyIter_Next(iterator))) { eid = -1; - if (igraphmodule_PyObject_to_eid(item, &eid, graph)) + if (igraphmodule_PyObject_to_eid(item, &eid, graph)) { break; + } Py_DECREF(item); - igraph_vector_push_back(&vector, eid); + + if (igraph_vector_push_back(&vector, eid)) { + igraphmodule_handle_igraph_error(); + /* no need to destroy 'vector' here; will be done outside the loop due + * to PyErr_Occurred */ + break; + } } + Py_DECREF(iterator); if (PyErr_Occurred()) { igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); return 1; } @@ -2707,17 +2748,19 @@ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, } igraph_vector_destroy(&vector); - IGRAPH_FINALLY_CLEAN(1); - if (return_single) + if (return_single) { *return_single = 0; + } return 0; } /* The object can be converted into a single edge ID */ - if (return_single) + if (return_single) { *return_single = 1; + } + /* if (single_eid) *single_eid = eid; diff --git a/tests/test_basic.py b/tests/test_basic.py index 28bca91e9..f7eb08e28 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -287,11 +287,15 @@ def testDeleteEdges(self): self.assertEqual(3, g.ecount()) self.assertEqual(el, g.get_edgelist()) + print("GGGG") + # Deleting nonexistent edges self.assertRaises(ValueError, g.delete_edges, [(0, 2)]) self.assertRaises(ValueError, g.delete_edges, [("A", "C")]) self.assertRaises(ValueError, g.delete_edges, [(0, 15)]) + print("HHHH") + # Delete all edges g.delete_edges() self.assertEqual(0, g.ecount()) diff --git a/vendor/source/igraph b/vendor/source/igraph index 55b9cdca9..213abc889 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 55b9cdca9aa47832f841aa1edaf9c13aa1d46a98 +Subproject commit 213abc889d360775dfd0c11a406e859827c6dec8 From a02662e69d6ef4fe5a00e284a8acbe81b042bb1c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Mar 2021 14:35:58 +0100 Subject: [PATCH 0233/1681] fix: remove unneeded print() statements from tests --- tests/test_basic.py | 4 ---- tests/test_generators.py | 1 - 2 files changed, 5 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index f7eb08e28..28bca91e9 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -287,15 +287,11 @@ def testDeleteEdges(self): self.assertEqual(3, g.ecount()) self.assertEqual(el, g.get_edgelist()) - print("GGGG") - # Deleting nonexistent edges self.assertRaises(ValueError, g.delete_edges, [(0, 2)]) self.assertRaises(ValueError, g.delete_edges, [("A", "C")]) self.assertRaises(ValueError, g.delete_edges, [(0, 15)]) - print("HHHH") - # Delete all edges g.delete_edges() self.assertEqual(0, g.ecount()) diff --git a/tests/test_generators.py b/tests/test_generators.py index 6895cac0a..1d566b54e 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -293,7 +293,6 @@ def testSparseWeighedAdjacency(self): el = g.get_edgelist() self.assertFalse(g.is_directed()) self.assertEqual(4, g.vcount()) - print(repr(el)) self.assertTrue(el == [(0, 1), (2, 2)]) self.assertTrue(g.es["weight"] == [1, 2.5]) From 167c1bb7f1275e28a25b687cb8fd8214ce587c94 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Mar 2021 14:44:02 +0100 Subject: [PATCH 0234/1681] fix: fix deprecation warnings when compiling the Python interface --- src/_igraph/attributes.c | 10 ++++------ src/_igraph/igraphmodule.c | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 124b34ed5..191b9124b 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -1840,16 +1840,14 @@ void igraphmodule_initialize_attribute_handler(void) { * Also raises a suitable Python exception if needed. */ int igraphmodule_attribute_name_check(PyObject* obj) { - PyObject* type_str; + PyObject* type_obj; if (obj != 0 && PyBaseString_Check(obj)) return 1; - type_str = obj ? PyObject_Str((PyObject*)obj->ob_type) : 0; - if (type_str != 0) { - PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only, got %s", - PyUnicode_AS_UNICODE(type_str)); - Py_DECREF(type_str); + type_obj = obj ? ((PyObject*)obj->ob_type) : 0; + if (type_obj != 0) { + PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only, got %R", type_obj); } else { PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only"); } diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 856d4bf7a..ea3661c3f 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -430,7 +430,7 @@ PyObject* igraphmodule_is_degree_sequence(PyObject *self, return NULL; } - if (igraph_is_degree_sequence(&out_deg, is_directed ? &in_deg : 0, &result)) { + if (igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW, &result)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&out_deg); if (is_directed) @@ -470,7 +470,7 @@ PyObject* igraphmodule_is_graphical_degree_sequence(PyObject *self, return NULL; } - if (igraph_is_graphical_degree_sequence(&out_deg, is_directed ? &in_deg : 0, &result)) { + if (igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, IGRAPH_SIMPLE_SW, &result)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&out_deg); if (is_directed) From 35a47ba43219255812571cd87ed8deef9f127e19 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Mar 2021 20:45:41 +0100 Subject: [PATCH 0235/1681] fix: use deferred import for matplotlib to cut down on import time --- src/igraph/drawing/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 74cf7bdc4..6237e77a3 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -41,7 +41,6 @@ __license__ = "GPL" cairo = find_cairo() -matplotlib, plt = find_matplotlib() ##################################################################### @@ -476,6 +475,8 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @see: Graph.__plot__ """ + _, plt = find_matplotlib() + if hasattr(plt, "Axes") and isinstance(target, plt.Axes): result = MatplotlibGraphDrawer(ax=target) result.draw(obj, *args, **kwds) From c89fe03ac0b43506d7809a8fcb7a2bf3a0a6a147 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Thu, 25 Mar 2021 10:36:18 +0100 Subject: [PATCH 0236/1681] Add GraphML support for Windows wheels (#375) --- .github/workflows/build.yml | 185 +++++++++++++++++++++--------------- appveyor.yml | 65 ------------- setup.py | 23 +++++ src/igraph/__init__.py | 2 - tests/test_foreign.py | 2 + tox.ini | 1 + 6 files changed, 136 insertions(+), 142 deletions(-) delete mode 100644 appveyor.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2c207e6d..e205bff6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,85 +1,120 @@ -name: "Build and test with tox" - -on: - push: - branches: - - '*' - tags-ignore: - - '*.*.*' - pull_request: - branches: - - '*' - tags-ignore: - - '*.*.*' - +name: Build and test, upload to PyPI on release +on: [push, pull_request] jobs: - build_linux: - runs-on: ubuntu-latest + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 - - pypy-3.7 + os: [ubuntu-20.04, macos-10.15] steps: - - uses: actions/checkout@v1 - - name: Init C core submodule - run: | - git submodule update --init - cat vendor/source/igraph/.git - cat .git/modules/vendor/source/igraph/HEAD - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install OS dependencies - run: - sudo apt-get install gfortran flex bison - - name: Install Python dependencies - run: | - # Pypi has no pip by default, and ubuntu blocks python -m ensurepip - # However, Github runners are supposed to have pip installed by default - # https://docs.github.com/en/actions/guides/building-and-testing-python - #wget -qO- https://bootstrap.pypa.io/get-pip.py | python - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox - env: - TESTING_IN_TOX: 1 - - build_osx: - runs-on: macos-latest - strategy: - matrix: - python-version: - - 3.6 - - 3.7 - - 3.8 - - 3.9 + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.8' + + - name: Install OS dependencies (macOS) + if: runner.os == 'macOS' + run: + brew install ninja autoconf automake libtool cmake + + - name: Build wheels + uses: joerick/cibuildwheel@v1.10.0 + env: + CIBW_BEFORE_BUILD_MACOS: "python setup.py build_c_core" + CIBW_BEFORE_BUILD_LINUX: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_SKIP: "cp27-* pp27-* cp35-*" + + - uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + + build_wheel_win: + name: Build wheels on Windows + runs-on: windows-2019 + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.8' + + - name: Install OS dependencies + run: | + choco install winflexbison3 ninja + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe integrate install + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install libxml2:x64-windows-static-md + shell: cmd + + - name: Build wheels + uses: joerick/cibuildwheel@v1.10.0 + env: + CIBW_BEFORE_BUILD: "pip install delvewheel && python setup.py build_c_core" + CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest" + CIBW_SKIP: "cp27-* pp27-* cp35-* *-win32" + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=x64-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake + IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/x64-windows-static-md/lib/ + IGRAPH_STATIC_EXTENSION: True + IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset + IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 + + - uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + - name: Install OS dependencies + run: + sudo apt install ninja-build cmake flex bison + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.8' + + - name: Build sdist + run: | + python setup.py build_c_core + python setup.py sdist + + - uses: actions/upload-artifact@v2 + with: + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_wheel_win, build_sdist] + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' steps: - - uses: actions/checkout@v1 - - name: Init C core submodule - run: git submodule update --init - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install OS dependencies - run: - brew install autoconf automake libtool - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install tox tox-gh-actions - - name: Test with tox - run: tox - env: - TESTING_IN_TOX: 1 + - uses: actions/download-artifact@v2 + with: + name: artifact + path: dist + - uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_password }} + # To test: repository_url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 30e1daab8..000000000 --- a/appveyor.yml +++ /dev/null @@ -1,65 +0,0 @@ -image: - - Visual Studio 2019 - -environment: - global: - CIBW_BEFORE_BUILD: python setup.py build_c_core - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - PYTHON: "C:\\Python37" - - matrix: - - CIBW_BUILD: "*-win32" - CIBW_SKIP: "pp27-* cp27-* cp35-*" - IGRAPH_CMAKE_EXTRA_ARGS: "-A Win32" - MSYSTEM: MINGW32 - PATH: C:\msys64\usr\bin;C:\msys64\mingw32\bin;C:\Windows\System32;C:\Windows;%PATH% - TARGET_ARCH: "x86" - - - CIBW_BUILD: "*-win_amd64" - CIBW_SKIP: "pp27-* cp27-* cp35-*" - IGRAPH_CMAKE_EXTRA_ARGS: "-A x64" - MSYSTEM: MINGW64 - PATH: C:\msys64\usr\bin;C:\msys64\mingw64\bin;C:\Windows\System32;C:\Windows;%PATH% - TARGET_ARCH: "x64" - -platform: - - x64 - -install: - # update msys2 - - bash -lc "pacman --needed --noconfirm -Sy pacman-mirrors" - - bash -lc "pacman --noconfirm -Sy" - - bash -lc "pacman --needed --noconfirm -S autoconf automake bison flex libtool mingw-w64-x86_64-libtool mingw-w64-i686-libxml2 mingw-w64-x86_64-libxml2 zip" - - # prepare Python - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "python --version" - - "python -m pip install --upgrade pip" - - # install cibuildwheel - - pip install cibuildwheel==1.6.1 - - # install pandas (optional) - - pip install pandas - -before_build: - - git submodule update --init --recursive - -for: - - matrix: - only: - - MSYSTEM: MINGW32 - build_script: - - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars32.bat" - - cibuildwheel --output-dir wheelhouse - - matrix: - only: - - MSYSTEM: MINGW64 - build_script: - - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" - - cibuildwheel --output-dir wheelhouse - -artifacts: - - path: "wheelhouse\\*.whl" - name: Wheels - diff --git a/setup.py b/setup.py index bcc104dfe..72f661952 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ def find_static_library(library_name, library_path): extra_libdirs = [ "/usr/local/lib64", "/usr/local/lib", + "/usr/lib/x86_64-linux-gnu", "/usr/lib64", "/usr/lib", "/lib64", @@ -391,6 +392,18 @@ def run(self): + buildcfg.library_dirs ) + # Add extra libraries that may have been specified + if "IGRAPH_EXTRA_LIBRARIES" in os.environ: + extra_libraries = os.environ["IGRAPH_EXTRA_LIBRARIES"].split(',') + buildcfg.libraries.extend(extra_libraries) + + # Override static specification based on environment variable + if "IGRAPH_STATIC_EXTENSION" in os.environ: + if os.environ["IGRAPH_STATIC_EXTENSION"].lower() in ['true', '1', 'on']: + buildcfg.static_extension = True + else: + buildcfg.static_extension = False + # Replaces library names with full paths to static libraries # where possible. libm.a is excluded because it caused problems # on Sabayon Linux where libm.a is probably not compiled with @@ -401,6 +414,11 @@ def run(self): else: buildcfg.replace_static_libraries(exclusions=["m"]) + # Add extra libraries that may have been specified + if "IGRAPH_EXTRA_DYNAMIC_LIBRARIES" in os.environ: + extra_libraries = os.environ["IGRAPH_EXTRA_DYNAMIC_LIBRARIES"].split(',') + buildcfg.libraries.extend(extra_libraries) + # Prints basic build information buildcfg.print_build_info() @@ -658,8 +676,11 @@ def replace_static_libraries(self, only=None, exclusions=None): static_lib = find_static_library(library_name, self.library_dirs) if static_lib: + print(f"Found {library_name} as static library in {static_lib}.") self.libraries.remove(library_name) self.extra_objects.append(static_lib) + else: + print(f"Warning: could not find static library of {library_name}.") def use_vendored_igraph(self): """Assumes that igraph is installed already in ``vendor/install/igraph`` and sets up @@ -730,6 +751,8 @@ def use_educated_guess(self): buildcfg = BuildConfiguration() buildcfg.process_args_from_command_line() + + # Define the extension sources = glob.glob(os.path.join("src", "_igraph", "*.c")) igraph_extension = Extension("igraph._igraph", sources) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 23d674b6d..547c56a85 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1,5 +1,3 @@ -# vim:ts=4:sw=4:sts=4:et -# -*- coding: utf-8 -*- """ IGraph library. """ diff --git a/tests/test_foreign.py b/tests/test_foreign.py index fc3d2ec69..f8782a3d0 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -340,6 +340,8 @@ def testPickle(self): self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) + + @skipIf(pd is None, "test case depends on Pandas") def testVertexDataFrames(self): g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) diff --git a/tox.ini b/tox.ini index 73f8166ec..ba72a3bcd 100644 --- a/tox.ini +++ b/tox.ini @@ -21,6 +21,7 @@ deps = numpy; platform_python_implementation != "PyPy" networkx; platform_python_implementation != "PyPy" pandas; platform_python_implementation != "PyPy" +passenv = PATH setenv = TESTING_IN_TOX=1 From 70c06b17e568af78fcd6330813620fd396aa0599 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Mar 2021 10:52:00 +0100 Subject: [PATCH 0237/1681] fix: removed unused x11torgb.py script --- scripts/x11torgb.py | 40 ---------------------------------------- 1 file changed, 40 deletions(-) delete mode 100755 scripts/x11torgb.py diff --git a/scripts/x11torgb.py b/scripts/x11torgb.py deleted file mode 100755 index f85402a8a..000000000 --- a/scripts/x11torgb.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -"""Converts X11 color files to RGB formatted Python dicts""" -import sys -import pprint - -if len(sys.argv)<2: - print "Usage: %s filename" % sys.argv[0] - sys.exit(1) - -colors = { - "black": (0. , 0. , 0. , 1.), - "silver": (0.75, 0.75, 0.75, 1.), - "gray": (0.5 , 0.5 , 0.5 , 1.), - "white": (1. , 1. , 1. , 1.), - "maroon": (0.5 , 0. , 0. , 1.), - "red": (1. , 0. , 0. , 1.), - "purple": (0.5 , 0. , 0.5 , 1.), - "fuchsia": (1. , 0. , 1. , 1.), - "green": (0. , 0.5 , 0. , 1.), - "lime": (0. , 1. , 0. , 1.), - "olive": (0.5 , 0.5 , 0. , 1.), - "yellow": (1. , 1. , 0. , 1.), - "navy": (0. , 0. , 0.5 , 1.), - "blue": (0. , 0. , 1. , 1.), - "teal": (0. , 0.5 , 0.5 , 1.), - "aqua": (0. , 1. , 1. , 1.), -} - -f = open(sys.argv[1]) -for line in f: - if line[0] == '!': continue - parts = line.strip().split(None, 3) - for x in xrange(3): - parts[x] = float(parts[x])/255. - parts[3:3] = [1.] - colors[parts[4].lower()] = tuple(parts[0:4]) - -pp = pprint.PrettyPrinter(indent=4) -pp.pprint(colors) - From 5e08c52e68d0e9a046cd5714fd81fee5c59304c0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 25 Mar 2021 10:56:08 +0100 Subject: [PATCH 0238/1681] doc: remove Appveyor badge from README [ci skip] --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index c65591029..21102ef74 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Build and test with tox](https://github.com/igraph/python-igraph/actions/workflows/build.yml/badge.svg)](https://github.com/igraph/python-igraph/actions/workflows/build.yml) -[![Build status](https://ci.appveyor.com/api/projects/status/55i1d4g65q11f9l5?svg=true)](https://ci.appveyor.com/project/ntamas/python-igraph-jst2e) [![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://pypi.python.org/pypi/python-igraph) [![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) From 2b9a6917c65e99a50d400834a37cb331f17b3d24 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Thu, 25 Mar 2021 20:48:59 +0100 Subject: [PATCH 0239/1681] Added unittest for GraphML (#377) --- tests/test_foreign.py | 62 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/tests/test_foreign.py b/tests/test_foreign.py index f8782a3d0..630a612fc 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -284,6 +284,66 @@ def testAdjacency(self): g.write_adjacency(tmpfname) + def testGraphML(self): + with temporary_file( + """\ + + + + + + + a + + + b + + + c + + + d + + + e + + + f + + + + + + + + + + + + + + + + + + """ + ) as tmpfname: + try: + g = Graph.Read_GraphML(tmpfname) + except NotImplementedError as e: + self.skipTest(str(e)) + + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(g.vcount(), 6) + self.assertEqual(g.ecount(), 7) + self.assertFalse(g.is_directed()) + self.assertTrue("name" in g.vertex_attributes()) + + g.write_graphml(tmpfname) + def testPickle(self): pickle = [ 128, @@ -340,8 +400,6 @@ def testPickle(self): self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) - - @skipIf(pd is None, "test case depends on Pandas") def testVertexDataFrames(self): g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) From 389c25595e5ff59e4315160850903b2b4792a4d2 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Fri, 26 Mar 2021 09:23:42 +0100 Subject: [PATCH 0240/1681] CI: Updated (#378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tamás Nepusz --- .flake8 | 3 + .github/workflows/build.yml | 127 ++++++++++++++++++++-------- .github/workflows/deploy.yml | 158 ----------------------------------- setup.py | 4 - tox.ini | 31 ------- 5 files changed, 95 insertions(+), 228 deletions(-) create mode 100644 .flake8 delete mode 100644 .github/workflows/deploy.yml delete mode 100644 tox.ini diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..e4e5eb32d --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e205bff6a..7cd1e6844 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,14 +1,17 @@ name: Build and test, upload to PyPI on release on: [push, pull_request] +env: + CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_SKIP: "cp27-* pp27-* cp35-*" jobs: build_wheels: - name: Build wheels on ${{ matrix.os }} - runs-on: ${{ matrix.os }} + name: Build wheels on Linux (${{ matrix.wheel_arch }}) + runs-on: ubuntu-20.04 strategy: matrix: - os: [ubuntu-20.04, macos-10.15] + wheel_arch: [x86_64, i686] steps: - uses: actions/checkout@v2 @@ -21,26 +24,64 @@ jobs: with: python-version: '3.8' - - name: Install OS dependencies (macOS) - if: runner.os == 'macOS' + - name: Build wheels + uses: joerick/cibuildwheel@v1.10.0 + env: + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" + CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" + + - uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + + build_wheel_macos: + name: Build wheels on macOS + runs-on: macos-10.15 + + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v2 + with: + path: vendor/install + key: C-core-${{ runner.os }}-${{ hashFiles('.gitmodules') }} + + - uses: actions/setup-python@v2 + name: Install Python + with: + python-version: '3.8' + + - name: Install OS dependencies + if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core run: brew install ninja autoconf automake libtool cmake - name: Build wheels uses: joerick/cibuildwheel@v1.10.0 env: - CIBW_BEFORE_BUILD_MACOS: "python setup.py build_c_core" - CIBW_BEFORE_BUILD_LINUX: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - CIBW_SKIP: "cp27-* pp27-* cp35-*" + CIBW_BEFORE_BUILD: "python setup.py build_c_core" - uses: actions/upload-artifact@v2 with: path: ./wheelhouse/*.whl build_wheel_win: - name: Build wheels on Windows + name: Build wheels on Windows (${{ matrix.cmake_arch }}) runs-on: windows-2019 + strategy: + matrix: + include: + - cmake_arch: Win32 + wheel_arch: win32 + vcpkg_arch: x86 + - cmake_arch: x64 + wheel_arch: win_amd64 + vcpkg_arch: x64 steps: - uses: actions/checkout@v2 @@ -53,31 +94,47 @@ jobs: with: python-version: '3.8' - - name: Install OS dependencies + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v2 + with: + path: vendor/install + key: C-core-build-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.gitmodules') }}- + + - name: Cache VCPKG + uses: actions/cache@v2 + with: + path: C:/vcpkg/installed/ + key: vcpkg-${{ runner.os }}-${{ matrix.vcpkg_arch }} + + - name: Install build dependencies + if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core + run: choco install winflexbison3 ninja + + - name: Install VCPKG libraries run: | - choco install winflexbison3 ninja %VCPKG_INSTALLATION_ROOT%\vcpkg.exe integrate install - %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install libxml2:x64-windows-static-md + %VCPKG_INSTALLATION_ROOT%\vcpkg.exe install libxml2:${{ matrix.vcpkg_arch }}-windows-static-md shell: cmd - name: Build wheels uses: joerick/cibuildwheel@v1.10.0 env: - CIBW_BEFORE_BUILD: "pip install delvewheel && python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest" - CIBW_SKIP: "cp27-* pp27-* cp35-* *-win32" - IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=x64-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake - IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/x64-windows-static-md/lib/ + CIBW_BEFORE_BUILD: "python setup.py build_c_core" + CIBW_BUILD: "*-${{ matrix.wheel_arch }}" + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} + IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 + CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest" - uses: actions/upload-artifact@v2 with: path: ./wheelhouse/*.whl build_sdist: - name: Build source distribution + name: Build sdist and test extra dependencies runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -85,7 +142,17 @@ jobs: submodules: true fetch-depth: 0 + - name: Cache installed C core + id: cache-c-core + uses: actions/cache@v2 + with: + path: | + vendor/build + vendor/install + key: C-core-${{ runner.os }}-${{ hashFiles('.gitmodules') }}-4 + - name: Install OS dependencies + if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core run: sudo apt install ninja-build cmake flex bison @@ -98,23 +165,13 @@ jobs: run: | python setup.py build_c_core python setup.py sdist + python setup.py install + + - name: Test + run: | + pip install numpy scipy pandas + python -m unittest - uses: actions/upload-artifact@v2 with: path: dist/*.tar.gz - - upload_pypi: - needs: [build_wheels, build_wheel_win, build_sdist] - runs-on: ubuntu-latest - if: github.event_name == 'release' && github.event.action == 'published' - steps: - - uses: actions/download-artifact@v2 - with: - name: artifact - path: dist - - - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_password }} - # To test: repository_url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 65ef5466e..000000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,158 +0,0 @@ -name: "Build wheels and deploy" - -# To trigger manually, make yourself an auth token including "workflow" scope and run: -# curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token " https://api.github.com/repos/igraph/python-igraph/actions/workflows/deploy.yml/dispatches -d '{"ref":"master"}' -# If you want to release from another branch and not "master", change the last string. - -on: - workflow_dispatch: - push: - tags: - - '*.*.*' - - -jobs: - wheels_manylinux: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Init C core submodule - run: git submodule update --init - - name: Set up Python - uses: actions/setup-python@v2 - - name: Install OS dependencies - run: - sudo apt-get install gfortran flex bison - - name: Install cibuildwheel - run: | - # Pypi has no pip by default, and ubuntu blocks python -m ensurepip - # However, Github runners are supposed to have pip installed by default - # https://docs.github.com/en/actions/guides/building-and-testing-python - #wget -qO- https://bootstrap.pypa.io/get-pip.py | python - python -m pip install --upgrade pip - pip install cibuildwheel - - name: Wheels (Linux) - env: - CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - CIBW_SKIP: "cp27-* pp27-* cp35-*" - run: | - python setup.py -q sdist - python -m cibuildwheel --output-dir wheelhouse - - name: Upload source dist - uses: actions/upload-artifact@v2 - with: - name: source-dist - path: dist/python-igraph-*.tar.gz - - name: Upload Linux wheels - uses: actions/upload-artifact@v2 - with: - name: linux-wheels - path: wheelhouse/*.whl - - wheels_osx: - runs-on: macos-latest - steps: - - uses: actions/checkout@v1 - - name: Init C core submodule - run: git submodule update --init - - name: Set up Python - uses: actions/setup-python@v2 - - name: Install OS dependencies - run: - brew install autoconf automake libtool - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install cibuildwheel - - name: Wheels (OSX) - run: | - python -m cibuildwheel --output-dir wheelhouse - env: - CIBW_BEFORE_BUILD: "pip install cmake && python setup.py build_c_core" - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" - CIBW_SKIP: "cp27-* pp27-* cp35-*" - - name: Upload Linux wheels - uses: actions/upload-artifact@v2 - with: - name: OSX-wheels - path: wheelhouse/*.whl - - create_release: - runs-on: ubuntu-latest - needs: - - wheels_manylinux - - wheels_osx - steps: - - name: "Download artifacts: source dist" - uses: actions/download-artifact@v2 - with: - name: source-dist - path: dist - - - name: "Download artifacts: Linux wheels" - uses: actions/download-artifact@v2 - with: - name: linux-wheels - path: wheelhouse - - - name: "Download artifacts: OSX wheels" - uses: actions/download-artifact@v2 - with: - name: OSX-wheels - path: wheelhouse - - - name: "List contents of artifact folders" - run: | - echo "dist folder:" - ls -lh dist - echo "wheelhouse folder:" - ls -lh wheelhouse - - - name: Store igraph version - id: get_version - run: | - echo "igraph_version=$(fn=$(ls dist); fn=${fn##*-}; echo ${fn%.tar.gz})" >> $GITHUB_ENV - tail -n 1 $GITHUB_ENV - - - name: Create release - id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token - with: - tag_name: ${{ env.igraph_version }} - release_name: Release ${{ env.igraph_version }} - draft: true - prerelease: false - - #- name: Upload binaries and source - # uses: AButler/upload-release-assets@v2.0 - # with: - # files: 'wheelhouse/*.whl;dist/python-igraph-*.tar.gz' - # repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Move artifacts into one folder - run: | - mkdir upload - mv dist/* upload/ - mv wheelhouse/* upload/ - ls upload - - - name: Upload binaries and source - uses: actions/github-script@v3 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - script: | - const path = require('path'); - const fs = require('fs'); - const release_id = '${{ steps.create_release.outputs.id }}'; - for (let file of await fs.readdirSync('./upload/')) { - console.log('uploadReleaseAsset', file); - await github.repos.uploadReleaseAsset({ - owner: context.repo.owner, - repo: context.repo.repo, - release_id: release_id, - name: file, - data: await fs.readFileSync(`./upload/${file}`) - }); - } diff --git a/setup.py b/setup.py index 72f661952..dcb4bab94 100644 --- a/setup.py +++ b/setup.py @@ -510,10 +510,6 @@ def run(self): parser_dir = os.path.join(igraph_build_dir, "src", "io", "parsers") if os.path.isdir(parser_dir): shutil.copytree(parser_dir, os.path.join(igraph_source_repo, "src", "io", "parsers")) - elif os.environ.get("TESTING_IN_TOX"): - # we allow to proceed when running in tox in the CI environment; - # bison and flex will be used to generate the parsers - pass else: raise RuntimeError(f"You need to build the C core of igraph first before generating a source tarball of python-igraph") diff --git a/tox.ini b/tox.ini deleted file mode 100644 index ba72a3bcd..000000000 --- a/tox.ini +++ /dev/null @@ -1,31 +0,0 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - -[tox] -envlist = py36, py37, py38, py39, pypy, pypy3 - -[gh-actions] -python = - 3.6: py36 - 3.7: py37 - 3.8: py38 - 3.9: py39 - pypy-3.7: pypy3 - -[testenv] -commands = python -m unittest -deps = - scipy; platform_python_implementation != "PyPy" - numpy; platform_python_implementation != "PyPy" - networkx; platform_python_implementation != "PyPy" - pandas; platform_python_implementation != "PyPy" -passenv = PATH -setenv = - TESTING_IN_TOX=1 - -[flake8] -max-line-length = 80 -select = C,E,F,W,B,B950 -ignore = W503,E501,E402,E203 From ace6896f6d7b501e8de9198ecac28d1d5c8e0e13 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Mar 2021 10:32:27 +0100 Subject: [PATCH 0241/1681] doc: fix a warning when generating the API docs [ci skip] --- src/igraph/drawing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 6237e77a3..e099c0dd8 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -320,7 +320,7 @@ def show(self): """Saves the plot to a temporary file and shows it. This method is deprecated from python-igraph 0.9.1 and will be removed in - 0.10.0. + version 0.10.0. @deprecated: Opening an image viewer with a temporary file never worked reliably across platforms. From bdafc25a49eb3fde703cb81aef5b18ca8dad5104 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Mar 2021 10:33:06 +0100 Subject: [PATCH 0242/1681] doc: scripts/mkdoc.sh now ensures that python-igraph is installed from a wheel (PyDoctor does not like the bootstrap scripts added by Python eggs) --- scripts/mkdoc.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 214027c4d..356d13d64 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -15,7 +15,7 @@ cd ${ROOT_FOLDER} if [ ! -d ".venv" ]; then # Create a virtual environment for pydoctor python3 -m venv .venv - .venv/bin/pip install pydoctor + .venv/bin/pip install -U pydoctor wheel fi PYDOCTOR=.venv/bin/pydoctor @@ -29,6 +29,14 @@ PWD=`pwd` echo "Removing existing documentation..." rm -rf "${DOC_API_FOLDER}/html" "${DOC_API_FOLDER}/pdf" +echo "Removing existing python-igraph eggs from virtualenv..." +SITE_PACKAGES_DIR=`.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])'` +rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg +rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg-link + +echo "Installing python-igraph in virtualenv..." +rm -f dist/*.whl && .venv/bin/python setup.py bdist_wheel && .venv/bin/pip install dist/*.whl + IGRAPHDIR=`.venv/bin/python3 -c 'import igraph, os; print(os.path.dirname(igraph.__file__))'` echo "Generating HTML documentation..." From 20d0b04343042759c9a8071651a5a0c0e4897149 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Mar 2021 10:39:54 +0100 Subject: [PATCH 0243/1681] test: restore tox.ini, it is useful to test things locally [ci skip] --- tox.ini | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..bd0355f4d --- /dev/null +++ b/tox.ini @@ -0,0 +1,27 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py36, py37, py38, py39, pypy, pypy3 + +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + pypy-3.7: pypy3 + +[testenv] +commands = python -m unittest +deps = + scipy; platform_python_implementation != "PyPy" + numpy; platform_python_implementation != "PyPy" + networkx; platform_python_implementation != "PyPy" + pandas; platform_python_implementation != "PyPy" +passenv = PATH +setenv = + TESTING_IN_TOX=1 + From 1137bc1cbc962e59ce7a3702e0c530e705f2c493 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Mar 2021 10:41:02 +0100 Subject: [PATCH 0244/1681] chore: bumped version to 0.9.1 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 16be2bd7f..4eeb3ae08 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.9.0' +version = '0.9.1' # The full version, including alpha/beta/rc tags. -release = '0.9.0' +release = '0.9.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index 1836d2e87..14ef391dc 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 9, 0) +__version_info__ = (0, 9, 1) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 13965a03fd3390ebd301a483c607d0657e4008de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Fri, 26 Mar 2021 11:31:44 +0100 Subject: [PATCH 0245/1681] remove unneeded square brackets from version numbers in changelog [ci skip] --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d15b63b1c..b2cc5c137 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## [0.9.1] +## 0.9.1 ### Changed @@ -30,7 +30,7 @@ 2.x are removed now that we have dropped support for Python 2.x. -## [0.9.0] +## 0.9.0 ### Added From 30f5a1bec0d44fdc95ca9ae8cc07c534bee1dd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Fri, 26 Mar 2021 11:44:37 +0100 Subject: [PATCH 0246/1681] added missing entry about GraphML support on Windows in changelog [ci skip] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2cc5c137..c8fea258e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,9 @@ * Many old code constructs that were used to maintain compatibility with Python 2.x are removed now that we have dropped support for Python 2.x. +* Reading GraphML files is now also supported on Windows if you use one of the + official Python wheels. + ## 0.9.0 From a343616fa0e3e2bdaaec7390c1419a5326caa3e8 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Sat, 27 Mar 2021 19:59:41 +0100 Subject: [PATCH 0247/1681] Improved documentation for compiling from source on Windows. (#380) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tamás Nepusz --- README.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 83 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 21102ef74..e276509f9 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ Python interface for the igraph library --------------------------------------- -igraph is a library for creating and manipulating graphs. +igraph is a library for creating and manipulating graphs. It is intended to be as powerful (ie. fast) as possible to enable the -analysis of large graphs. +analysis of large graphs. This repository contains the source code to the Python interface of igraph. @@ -23,7 +23,7 @@ running the following command should work without having to compile anything during installation: ``` -$ pip install python-igraph +pip install python-igraph ``` See details in [Installing Python Modules](https://docs.python.org/3/installing/). @@ -34,18 +34,72 @@ If you need to compile python-igraph from source for some reason, you need to install some dependencies first: ``` -$ sudo apt install build-essential python-dev libxml2 libxml2-dev zlib1g-dev bison flex +sudo apt install build-essential python-dev libxml2 libxml2-dev zlib1g-dev ``` and then run ``` -$ pip install python-igraph +pip install python-igraph ``` This should compile the C core of igraph as well as the Python extension automatically. +### Installation from source on Windows + +It is now also possible to compile `python-igraph` from source under Windows for +Python 3.6 and later. Make sure that you have Microsoft Visual Studio 2015 or +later installed, and of course Python 3.6 or later. First extract the source to +a suitable directory. If you launch the Developer command prompt and navigate to +the directory where you extracted the source code, you should be able to build +and install python-igraph using `python setup.py install` + +You may need to set the architecture that you are building on explicitly by setting the environment variable + +``` +set IGRAPH_CMAKE_EXTRA_ARGS=-A [arch] +``` + +where `[arch]` is either `Win32` for 32-bit builds or `x64` for 64-bit builds. + +#### Enabling GraphML + +By default, GraphML is disabled, because `libxml2` is not available on Windows in +the standard installation. You can install `libxml2` on Windows using +[`vcpkg`](https://github.com/Microsoft/vcpkg). After installation of `vcpkg` you +can install `libxml2` as follows + +``` +vcpkg.exe install libxml2:x64-windows-static-md +``` + +for 64-bit version (for 32-bit versions you can use the `x86-windows-static-md` +triplet). You need to integrate `vcpkg` in the build environment using + +``` +vcpkg.exe integrate install +``` + +This mentions that + +> CMake projects should use: `-DCMAKE_TOOLCHAIN_FILE=[vcpkg build script]` + +which we will do next. In order to build `python-igraph` correctly, you also +need to set some other environment variables before building `python-igraph`: + +``` +set IGRAPH_CMAKE_EXTRA_ARGS=-DVCPKG_TARGET_TRIPLET=x64-windows-static-md -DCMAKE_TOOLCHAIN_FILE=[vcpkg build script] +set IGRAPH_EXTRA_LIBRARY_PATH=[vcpkg directory]/installed/x64-windows-static-md/lib/ +set IGRAPH_STATIC_EXTENSION=True +set IGRAPH_EXTRA_LIBRARIES=libxml2,lzma,zlib,iconv,charset +set IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 +``` + +You can now build and install `python-igraph` again by simply running `python +setup.py build`. Please make sure to use a clean source tree, if you built +previously without GraphML, it will not update the build. + ### Linking to an existing igraph installation The source code of the Python package includes the source code of the matching @@ -57,23 +111,23 @@ custom installation of igraph is discoverable with `pkg-config`. First, check whether `pkg-config` can tell you the required compiler and linker flags for igraph: -``` -$ pkg-config --cflags --libs igraph +```bash +pkg-config --cflags --libs igraph ``` If `pkg-config` responds with a set of compiler and linker flags and not an error message, you are probably okay. You can then proceed with the installation using pip: -``` -$ pip install python-igraph --install-option="--use-pkg-config" +```bash +pip install python-igraph --install-option="--use-pkg-config" ``` Alternatively, if you have already downloaded and extracted the source code of igraph, you can run `setup.py` directly: -``` -$ python setup.py build --use-pkg-config +```bash +python setup.py build --use-pkg-config ``` This option is primarily intended for package maintainers in Linux @@ -83,6 +137,9 @@ the packaged igraph library instead of bringing its own copy. It is also useful on macOS if you want to link to the igraph library installed from Homebrew. +Due to the lack of support of `pkg-config` on Window, it is currently not +possible to build against an external library on Windows. + ## Compiling the development version If you have downloaded the source code from Github and not PyPI, chances are @@ -90,15 +147,25 @@ that you have the latest development version, which contains a matching version of the C core of igraph as a git submodule. Therefore, to install the bleeding edge version, you need to instruct git to check out the submodules first: -``` +```bash git submodule update --init ``` +Compiling the development version additionally requires `flex` and `bison`. You +can install those on Ubuntu using + +```bash +sudo apt install bison flex +``` + +On macOS you can install these from Homebrew or MacPorts. On Windows you can +install `winflexbison3` from Chocolatey. + Then, running the setup script should work if you have a C compiler and the necessary build dependencies (see the previous section): -``` -$ sudo python setup.py build +```bash +python setup.py build ``` ## Running unit tests @@ -106,8 +173,8 @@ $ sudo python setup.py build Unit tests can be executed from the project directory with `tox` or with the built-in unittest module: -``` -$ python -m unittest +```bash +python -m unittest ``` ## Contributing From 2a9c85831a89a11fce0c8b500794f60c94bcde42 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 28 Mar 2021 22:11:28 +0200 Subject: [PATCH 0248/1681] test: print index of graph when hub or authority score calculation fails in the Graph Atlas unit tests --- tests/test_atlas.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/test_atlas.py b/tests/test_atlas.py index 322a61f89..b1327550c 100644 --- a/tests/test_atlas.py +++ b/tests/test_atlas.py @@ -102,7 +102,16 @@ def testEigenvectorCentrality(self): def testHubScore(self): for idx, g in enumerate(self.__class__.graphs): - sc = g.hub_score() + try: + sc = g.hub_score() + except Exception as ex: + self.assertTrue( + False, + msg="Hub score calculation threw exception for graph #%d: %s" + % (idx, ex), + ) + raise + if g.vcount() == 0: self.assertEqual([], sc) continue @@ -119,7 +128,16 @@ def testHubScore(self): def testAuthorityScore(self): for idx, g in enumerate(self.__class__.graphs): - sc = g.authority_score() + try: + sc = g.authority_score() + except Exception as ex: + self.assertTrue( + False, + msg="Authority score calculation threw exception for graph #%d: %s" + % (idx, ex), + ) + raise + if g.vcount() == 0: self.assertEqual([], sc) continue From 4b2ed40b18f59cd0885c3c61bbf6dd70f19b455c Mon Sep 17 00:00:00 2001 From: Willem van den Boom Date: Tue, 30 Mar 2021 16:40:17 +0800 Subject: [PATCH 0249/1681] Fix links to Python documentation (#381) --- doc/source/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 84480fa6c..d72d0ee26 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -1050,6 +1050,6 @@ of the `Graph class`_. Should you get stuck, try asking in our `Discourse group`_ first - maybe there is someone out there who can help you out immediately. -.. _API documentation: https://igraph.org/python/doc/igraph-module.html -.. _Graph class: https://igraph.org/python/doc/igraph.Graph-class.html +.. _API documentation: https://igraph.org/python/doc/api/index.html +.. _Graph class: https://igraph.org/python/doc/api/igraph.Graph.html .. _Discourse group: https://igraph.discourse.group From 939c601ec46f2e563b086c62938e838c26749d6f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 30 Mar 2021 10:41:29 +0200 Subject: [PATCH 0250/1681] doc: make sure that an up-to-date wheel is installed when building docs [ci skip] --- scripts/mkdoc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 356d13d64..96826951c 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -35,7 +35,7 @@ rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg-link echo "Installing python-igraph in virtualenv..." -rm -f dist/*.whl && .venv/bin/python setup.py bdist_wheel && .venv/bin/pip install dist/*.whl +rm -f dist/*.whl && .venv/bin/python setup.py bdist_wheel && .venv/bin/pip install --force-reinstall dist/*.whl IGRAPHDIR=`.venv/bin/python3 -c 'import igraph, os; print(os.path.dirname(igraph.__file__))'` From 5b77225b35e28c35bb4948157d3004fda6a38c79 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 30 Mar 2021 10:41:44 +0200 Subject: [PATCH 0251/1681] doc: fix a docstring in EdgeSeq.select() [ci skip] --- src/_igraph/edgeseqobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 96ee3f19f..dc7457784 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -825,7 +825,7 @@ PyMethodDef igraphmodule_EdgeSeq_methods[] = { }, {"select", (PyCFunction)igraphmodule_EdgeSeq_select, METH_VARARGS, - "select(...)\n--\n\n" + "select(*args, **kwds)\n--\n\n" "For internal use only.\n" }, {NULL} From a47e005fbb5c2a3cac83df1314a2b2563749ec17 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 30 Mar 2021 10:49:46 +0200 Subject: [PATCH 0252/1681] fix: more descriptive error message when cairo or cairocffi is not installed, refs #283 --- src/igraph/drawing/utils.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index cde3e5266..699c1cad9 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -6,7 +6,7 @@ from math import atan2, cos, sin from operator import itemgetter -__all__ = ("BoundingBox", "FakeModule", "Point", "Rectangle") +__all__ = ("BoundingBox", "Point", "Rectangle") __license__ = "GPL" ##################################################################### @@ -403,17 +403,28 @@ def __or__(self, other): # pylint: disable-msg=R0903 # R0903: too few public methods -class FakeModule(object): +class FakeModule: """Fake module that raises an exception for everything""" + def __init__(self, message): + """Constructor. + + Parameters: + message: message to print in exceptions raised from this module + """ + self._message = message + def __getattr__(self, _): - raise AttributeError("plotting not available") + raise AttributeError(self._message) def __call__(self, _): - raise TypeError("plotting not available") + raise TypeError(self._message) def __setattr__(self, key, value): - raise AttributeError("plotting not available") + if key == "_message": + super().__setattr__(key, value) + else: + raise AttributeError(self._message) ##################################################################### @@ -425,7 +436,7 @@ def find_cairo(): Returns a fake module if everything fails. """ module_names = ["cairo", "cairocffi"] - module = FakeModule() + module = FakeModule("Plotting not available; please install pycairo or cairocffi") for module_name in module_names: try: module = __import__(module_name) @@ -439,8 +450,7 @@ def find_cairo(): def find_matplotlib(): - """Tries to import the ``cairo`` Python module if it is installed, - also trying ``cairocffi`` (a drop-in replacement of ``cairo``). + """Tries to import the ``matplotlib`` Python module if it is installed. Returns a fake module if everything fails. """ try: @@ -448,13 +458,13 @@ def find_matplotlib(): has_mpl = True except ImportError: - mpl = FakeModule() + mpl = FakeModule("You need to install matplotlib to use this functionality") has_mpl = False if has_mpl: import matplotlib.pyplot as plt else: - plt = FakeModule() + plt = FakeModule("You need to install matplotlib.pyplot to use this functionality") return mpl, plt From 1ad768a99ea54731ef812044a68ef79621c5b8fc Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Wed, 7 Apr 2021 22:49:20 +0200 Subject: [PATCH 0253/1681] CI: Remove parallel option, explicitly install release config. --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index dcb4bab94..c623e2e37 100644 --- a/setup.py +++ b/setup.py @@ -265,13 +265,13 @@ def compile_in(self, source_folder, build_folder, install_folder): print("Running build...") retcode = subprocess.call( - [cmake, "--build", ".", "--parallel", "--config", "Release"] + [cmake, "--build", ".", "--config", "Release"] ) if retcode: return False print("Installing build...") - retcode = subprocess.call([cmake, "--install", ".", "--prefix", str(install_folder)]) + retcode = subprocess.call([cmake, "--install", ".", "--prefix", str(install_folder), "--config", "Release"]) if retcode: return False From 91eb86c65ff1fd9af432dddf181e9262288ecd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Fri, 9 Apr 2021 09:43:26 +0200 Subject: [PATCH 0254/1681] doc: added a comment in setup.py explaining why we do not use parallel CMake builds --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c623e2e37..ffa7dedac 100644 --- a/setup.py +++ b/setup.py @@ -264,6 +264,7 @@ def compile_in(self, source_folder, build_folder, install_folder): return False print("Running build...") + # We are _not_ using a parallel build; this is intentional, see igraph/igraph#1755 retcode = subprocess.call( [cmake, "--build", ".", "--config", "Release"] ) From 1cd379ee46dcb4ccee2b820ec883bbe75674f7f6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 19 Apr 2021 09:40:05 +0200 Subject: [PATCH 0255/1681] doc: documentation fix to prevent a PyDoctor warning [ci skip] --- src/igraph/drawing/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 699c1cad9..68f1b9b60 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -409,8 +409,7 @@ class FakeModule: def __init__(self, message): """Constructor. - Parameters: - message: message to print in exceptions raised from this module + @param message: message to print in exceptions raised from this module """ self._message = message From 405098376535d453b01c0ba8a3ad44a02423fe89 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 14:05:26 +1000 Subject: [PATCH 0256/1681] Realize_Degree_Sequence implementation with strings --- doc/source/generation.rst | 1 + src/_igraph/graphobject.c | 97 +++++++++++++++++++++++++++++++++++++++ src/_igraph/graphobject.h | 1 + tests/test_generators.py | 27 +++++++++++ 4 files changed, 126 insertions(+) diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 2e689dd36..4659406a1 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -170,5 +170,6 @@ Finally, there are some ways of generating graphs that are not covered by the pr - De Bruijn graphs :meth:`Graph.De_Bruijn` - Lederberg-Coxeter-Frucht graphs :meth:`Graph.LCF` - graphs with a specified isomorphism class :meth:`Graph.Isoclass` + - graphs with a specified degree sequence :meth:`Graph.Realize_Degree_Sequence` .. _API documentation: https://igraph.org/python/doc/igraph-module.html diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 0ced8a25f..114c101bb 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2791,6 +2791,88 @@ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, return (PyObject *) self; } +/** \ingroup python_interface_graph + * \brief Generates a graph with a specified degree sequence + * \return a reference to the newly generated Python igraph object + * \sa igraph_realize_degree_sequence + */ +PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, + PyObject *args, PyObject *kwds) { + + igraph_vector_t outdeg, indeg; + igraph_vector_t *indegp = 0; + igraph_edge_type_sw_t allowed_edge_types; + igraph_realize_degseq_t method; + PyObject *outdeg_o, *indeg_o, *edge_types_o, *method_o; + PyObject *repr; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "outdeg", "indeg", "allowed_edge_types", "method", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOO", kwlist, + &outdeg_o, &indeg_o, &edge_types_o, &method_o)) + return NULL; + + /* allowed edge types */ + repr = PyObject_Str(edge_types_o); + if (PyUnicode_CompareWithASCIIString(repr, "simple_sw") == 0) + allowed_edge_types = IGRAPH_SIMPLE_SW; + else if (PyUnicode_CompareWithASCIIString(repr, "multi_sw") == 0) + allowed_edge_types = IGRAPH_MULTI_SW; + else { + Py_XDECREF(repr); + PyErr_SetString(PyExc_ValueError, "allowed_edge_types must be 'simple_sw' or 'multi_sw' (undirected only)."); + return NULL; + } + Py_XDECREF(repr); + + /* methods */ + repr = PyObject_Str(method_o); + if (PyUnicode_CompareWithASCIIString(repr, "smallest") == 0) + method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; + else if (PyUnicode_CompareWithASCIIString(repr, "largest") == 0) + method = IGRAPH_REALIZE_DEGSEQ_LARGEST; + else if (PyUnicode_CompareWithASCIIString(repr, "index") == 0) + method = IGRAPH_REALIZE_DEGSEQ_INDEX; + else { + Py_XDECREF(repr); + PyErr_SetString(PyExc_ValueError, "method must be 'smallest', 'largest', or 'index'"); + return NULL; + } + Py_XDECREF(repr); + + /* Outdegree vector */ + if (igraphmodule_PyObject_to_vector_t(outdeg_o, &outdeg, 0)) + return NULL; + + /* Indegree vector, PyNone means undirected graph */ + if (indeg_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(indeg_o, &indeg, 0)) { + igraph_vector_destroy(&outdeg); + return NULL; + } + indegp = &indeg; + } + + /* C function takes care of multi-sw and directed corner case */ + if (igraph_realize_degree_sequence(&g, &outdeg, indegp, allowed_edge_types, method)) { + igraph_vector_destroy(&outdeg); + if (indegp != 0) + igraph_vector_destroy(&indeg); + igraphmodule_handle_igraph_error(); + return NULL; + } + + igraph_vector_destroy(&outdeg); + if (indegp != 0) + igraph_vector_destroy(&indeg); + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + + /** \ingroup python_interface_graph * \brief Generates a graph based on vertex types and connection preferences * \return a reference to the newly generated Python igraph object @@ -12605,6 +12687,21 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param repeats: the number of repeats\n" }, + {"Realize_Degree_Sequence", (PyCFunction) igraphmodule_Graph_Realize_Degree_Sequence, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Realize_Degree_Sequence(outdeg, indeg, allowed_edge_types, method)\n--\n\n" + "Generates a graph from a degree sequence.\n\n" + "@param outdeg: the degree sequence of an undirected graph (if indeg=None), " + "or the out-degree sequence of a directed graph.\n" + "@param indeg: None to generate an undirected graph, the in-degree sequence " + "to generate a directed graph.\n" + "@param allowed_edge_types: TODO!\n" + "@param method: possible values are\n" + " - REALIZE_DEGSEQ_SMALLEST: The vertex with smallest remaining degree first.\n" + " - REALIZE_DEGSEQ_LARGEST: The vertex with the largest remaining degree first.\n" + " - REALIZE_DEGSEQ_INDEX: The vertices are selected in order of their index.\n" + }, + // interface to igraph_ring {"Ring", (PyCFunction) igraphmodule_Graph_Ring, METH_VARARGS | METH_CLASS | METH_KEYWORDS, diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 618dbb97b..3ab836942 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -92,6 +92,7 @@ PyObject* igraphmodule_Graph_Growing_Random(PyTypeObject *type, PyObject *args, PyObject* igraphmodule_Graph_Isoclass(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Lattice(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_LCF(PyTypeObject *type, PyObject *args, PyObject *kwds); +PyObject* igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Preference(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Recent_Degree(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Ring(PyTypeObject *type, PyObject *args, PyObject *kwds); diff --git a/tests/test_generators.py b/tests/test_generators.py index 1d566b54e..7b6810ac9 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -134,6 +134,33 @@ def testLCF(self): self.assertTrue(g1.isomorphic(g2)) self.assertRaises(InternalError, Graph.LCF, 12, (5, -5), -3) + def testRealizeDegreeSequence(self): + g = Graph.Realize_Degree_Sequence( + [1, 1], None, "simple_sw", "smallest", + ) + self.assertFalse(g.is_directed()) + + g = Graph.Realize_Degree_Sequence( + [1, 1], None, "multi_sw", "index", + ) + self.assertFalse(g.is_directed()) + + g = Graph.Realize_Degree_Sequence( + [1, 1], [1, 1], "simple_sw", "largest", + ) + self.assertTrue(g.is_directed()) + + # Not implemented, should fail + self.assertRaises(NotImplementedError, Graph.Realize_Degree_Sequence, + [1, 1], [1, 1], "multi_sw", "largest") + + self.assertRaises(ValueError, Graph.Realize_Degree_Sequence, + [1, 1], [1, 1], "should_fail", "index") + self.assertRaises(ValueError, Graph.Realize_Degree_Sequence, + [1, 1], [1, 1], "multi_sw", "should_fail") + + + def testKautz(self): g = Graph.Kautz(2, 2) deg_in = g.degree(mode=IN) From 4a1966c94445e6a9ca0b6034f61f842045a9fcec Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 14:12:25 +1000 Subject: [PATCH 0257/1681] Implement all 4 cases for multi-edges and loops, and add docs --- src/_igraph/graphobject.c | 17 +++++++++++++---- tests/test_generators.py | 9 +++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 114c101bb..8e6c56b54 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2817,8 +2817,12 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, repr = PyObject_Str(edge_types_o); if (PyUnicode_CompareWithASCIIString(repr, "simple_sw") == 0) allowed_edge_types = IGRAPH_SIMPLE_SW; + else if (PyUnicode_CompareWithASCIIString(repr, "loops_sw") == 0) + allowed_edge_types = IGRAPH_LOOPS_SW; else if (PyUnicode_CompareWithASCIIString(repr, "multi_sw") == 0) allowed_edge_types = IGRAPH_MULTI_SW; + else if (PyUnicode_CompareWithASCIIString(repr, "both_sw") == 0) + allowed_edge_types = IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW; else { Py_XDECREF(repr); PyErr_SetString(PyExc_ValueError, "allowed_edge_types must be 'simple_sw' or 'multi_sw' (undirected only)."); @@ -12695,11 +12699,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "or the out-degree sequence of a directed graph.\n" "@param indeg: None to generate an undirected graph, the in-degree sequence " "to generate a directed graph.\n" - "@param allowed_edge_types: TODO!\n" + "@param allowed_edge_types: for directed graphs, only 'simple_sw' is currently \n" + "implemented. Possible values for undirected graphs are\n" + " - 'simple_sw: simple graphs (no self-loops, no multi-edges)\n" + " - 'loops_sw': single self-loops allowed, but not multi-edges \n" + " - 'multi_sw': multi-edges allowed, but not self-loops \n" + " - 'both_sw': multi-edges and self-loops allowed \n" "@param method: possible values are\n" - " - REALIZE_DEGSEQ_SMALLEST: The vertex with smallest remaining degree first.\n" - " - REALIZE_DEGSEQ_LARGEST: The vertex with the largest remaining degree first.\n" - " - REALIZE_DEGSEQ_INDEX: The vertices are selected in order of their index.\n" + " - 'smallest': The vertex with smallest remaining degree first.\n" + " - 'largest': The vertex with the largest remaining degree first.\n" + " - 'index': The vertices are selected in order of their index.\n" }, // interface to igraph_ring diff --git a/tests/test_generators.py b/tests/test_generators.py index 7b6810ac9..b9bf37301 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -140,6 +140,15 @@ def testRealizeDegreeSequence(self): ) self.assertFalse(g.is_directed()) + # Not implemented, should fail + self.assertRaises(NotImplementedError, Graph.Realize_Degree_Sequence, + [1, 1], None, "loops_sw", "largest") + + g = Graph.Realize_Degree_Sequence( + [1, 1], None, "both_sw", "largest", + ) + self.assertFalse(g.is_directed()) + g = Graph.Realize_Degree_Sequence( [1, 1], None, "multi_sw", "index", ) From bdcae0cb0b9d7cae4017540384a73b52710231b2 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 17:38:45 +1000 Subject: [PATCH 0258/1681] use convert.c, but fails to raise error? --- src/_igraph/convert.c | 34 ++++++++++++++++++++++++++++++++++ src/_igraph/convert.h | 2 ++ src/_igraph/graphobject.c | 31 +++---------------------------- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 010f83877..d694ff57c 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -3025,3 +3025,37 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t return igraphmodule_PyObject_to_enum(o, pagerank_algo_tt, (int*)result); } + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_edge_type_sw_t + */ +int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result) { + static igraphmodule_enum_translation_table_entry_t edge_type_sw_tt[] = { + {"simple_sw", IGRAPH_SIMPLE_SW}, + {"loops_sw", IGRAPH_LOOPS_SW}, + {"multi_sw", IGRAPH_MULTI_SW}, + {"both_sw", IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW}, + {0,0} + }; + + return igraphmodule_PyObject_to_enum(o, edge_type_sw_tt, (int*)result); +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_realize_degseq_t + */ +int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq_t *result) { + static igraphmodule_enum_translation_table_entry_t realize_degseq_tt[] = { + {"smallest", IGRAPH_REALIZE_DEGSEQ_SMALLEST}, + {"largest", IGRAPH_REALIZE_DEGSEQ_LARGEST}, + {"index", IGRAPH_REALIZE_DEGSEQ_INDEX}, + {0,0} + }; + + return igraphmodule_PyObject_to_enum(o, realize_degseq_tt, (int*)result); +} + + + diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 8834420cf..0d1d117a8 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -63,6 +63,8 @@ int igraphmodule_PyObject_to_get_adjacency_t(PyObject *o, igraph_get_adjacency_t int igraphmodule_PyObject_to_layout_grid_t(PyObject *o, igraph_layout_grid_t *result); int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); +int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result); +int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq_t *result); int igraphmodule_PyObject_to_random_walk_stuck_t(PyObject *o, igraph_random_walk_stuck_t *result); int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *result); int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8e6c56b54..5bf29bc19 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2804,7 +2804,6 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, igraph_edge_type_sw_t allowed_edge_types; igraph_realize_degseq_t method; PyObject *outdeg_o, *indeg_o, *edge_types_o, *method_o; - PyObject *repr; igraphmodule_GraphObject *self; igraph_t g; @@ -2814,36 +2813,12 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, return NULL; /* allowed edge types */ - repr = PyObject_Str(edge_types_o); - if (PyUnicode_CompareWithASCIIString(repr, "simple_sw") == 0) - allowed_edge_types = IGRAPH_SIMPLE_SW; - else if (PyUnicode_CompareWithASCIIString(repr, "loops_sw") == 0) - allowed_edge_types = IGRAPH_LOOPS_SW; - else if (PyUnicode_CompareWithASCIIString(repr, "multi_sw") == 0) - allowed_edge_types = IGRAPH_MULTI_SW; - else if (PyUnicode_CompareWithASCIIString(repr, "both_sw") == 0) - allowed_edge_types = IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW; - else { - Py_XDECREF(repr); - PyErr_SetString(PyExc_ValueError, "allowed_edge_types must be 'simple_sw' or 'multi_sw' (undirected only)."); - return NULL; - } - Py_XDECREF(repr); + if (igraphmodule_PyObject_to_edge_type_sw_t(edge_types_o, &allowed_edge_types)) + return NULL; /* methods */ - repr = PyObject_Str(method_o); - if (PyUnicode_CompareWithASCIIString(repr, "smallest") == 0) - method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; - else if (PyUnicode_CompareWithASCIIString(repr, "largest") == 0) - method = IGRAPH_REALIZE_DEGSEQ_LARGEST; - else if (PyUnicode_CompareWithASCIIString(repr, "index") == 0) - method = IGRAPH_REALIZE_DEGSEQ_INDEX; - else { - Py_XDECREF(repr); - PyErr_SetString(PyExc_ValueError, "method must be 'smallest', 'largest', or 'index'"); + if (igraphmodule_PyObject_to_realize_degseq_t(method_o, &method)) return NULL; - } - Py_XDECREF(repr); /* Outdegree vector */ if (igraphmodule_PyObject_to_vector_t(outdeg_o, &outdeg, 0)) From 0efc6c17b9897f956f1bf47ea7c93dee78f8b062 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 18:26:39 +1000 Subject: [PATCH 0259/1681] Add strict PyObject to enum conversion --- src/_igraph/convert.c | 57 +++++++++++++++++++++++++++++++++++++++---- src/_igraph/convert.h | 2 ++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index d694ff57c..9a2f9f80c 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -118,6 +118,56 @@ int igraphmodule_PyObject_to_enum(PyObject *o, return -1; } + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to a corresponding igraph enum, strictly. + * + * The numeric value is returned as an integer that must be converted + * explicitly to the corresponding igraph enum type. This is to allow one + * to use the same common conversion routine for multiple enum types. + * + * \param o a Python object to be converted + * \param translation the translation table between strings and the + * enum values. Strings are treated as case-insensitive, but it is + * assumed that the translation table keys are lowercase. The last + * entry of the table must contain NULL values. + * \param result the result is returned here. The default value must be + * passed in before calling this function, since this value is + * returned untouched if the given Python object is Py_None. + * \return 0 if everything is OK, 1 otherwise. An appropriate exception + * is raised in this case. + */ +int igraphmodule_PyObject_to_enum_strict(PyObject *o, + igraphmodule_enum_translation_table_entry_t* table, + int *result) { + char *s; + + if (o == 0 || o == Py_None) + return 0; + if (PyLong_Check(o)) + return PyLong_AsInt(o, result); + s = PyUnicode_CopyAsString(o); + if (s == 0) { + PyErr_SetString(PyExc_TypeError, "int, long or string expected"); + return -1; + } + /* Do NOT convert string to lowercase */ + /* Search for exact matches */ + while (table->name != 0) { + if (strcmp(s, table->name) == 0) { + *result = table->value; + free(s); + return 0; + } + table++; + } + free(s); + PyErr_SetObject(PyExc_ValueError, o); + return -1; +} + + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_neimode_t @@ -3039,7 +3089,7 @@ int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t * {0,0} }; - return igraphmodule_PyObject_to_enum(o, edge_type_sw_tt, (int*)result); + return igraphmodule_PyObject_to_enum_strict(o, edge_type_sw_tt, (int*)result); } /** @@ -3054,8 +3104,5 @@ int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq {0,0} }; - return igraphmodule_PyObject_to_enum(o, realize_degseq_tt, (int*)result); + return igraphmodule_PyObject_to_enum_strict(o, realize_degseq_tt, (int*)result); } - - - diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 0d1d117a8..24af08abf 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -47,6 +47,8 @@ int PyLong_AsInt(PyObject* obj, int* result); int igraphmodule_PyObject_to_enum(PyObject *o, igraphmodule_enum_translation_table_entry_t *table, int *result); +int igraphmodule_PyObject_to_enum_strict(PyObject *o, + igraphmodule_enum_translation_table_entry_t *table, int *result); int igraphmodule_PyObject_to_add_weights_t(PyObject *o, igraph_add_weights_t *result); int igraphmodule_PyObject_to_adjacency_t(PyObject *o, igraph_adjacency_t *result); int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, From d639eff315a64bf1046930860e39902000fed19c Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 18:33:17 +1000 Subject: [PATCH 0260/1681] Restore case-insensitivity --- src/_igraph/convert.c | 6 ++++-- tests/test_generators.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 9a2f9f80c..2ceb9973a 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -141,7 +141,7 @@ int igraphmodule_PyObject_to_enum(PyObject *o, int igraphmodule_PyObject_to_enum_strict(PyObject *o, igraphmodule_enum_translation_table_entry_t* table, int *result) { - char *s; + char *s, *s2; if (o == 0 || o == Py_None) return 0; @@ -152,7 +152,9 @@ int igraphmodule_PyObject_to_enum_strict(PyObject *o, PyErr_SetString(PyExc_TypeError, "int, long or string expected"); return -1; } - /* Do NOT convert string to lowercase */ + /* Convert string to lowercase */ + for (s2=s; *s2; s2++) + *s2 = tolower(*s2); /* Search for exact matches */ while (table->name != 0) { if (strcmp(s, table->name) == 0) { diff --git a/tests/test_generators.py b/tests/test_generators.py index b9bf37301..49d515df9 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -135,8 +135,9 @@ def testLCF(self): self.assertRaises(InternalError, Graph.LCF, 12, (5, -5), -3) def testRealizeDegreeSequence(self): + # Test case insensitivity of options too g = Graph.Realize_Degree_Sequence( - [1, 1], None, "simple_sw", "smallest", + [1, 1], None, "simple_SW", "smallest", ) self.assertFalse(g.is_directed()) From 6a99a6ca34d74c588d9359f52e74a4a37f48d9f8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 19 Apr 2021 13:50:38 +0200 Subject: [PATCH 0261/1681] minor tweaks to Realize_Degree_Sequence(), extended tests --- src/_igraph/convert.c | 8 +++--- src/_igraph/graphobject.c | 50 ++++++++++++++++++++--------------- tests/test_generators.py | 55 ++++++++++++++++++++++++++------------- 3 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 2ceb9973a..52adff732 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -3084,10 +3084,10 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t */ int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result) { static igraphmodule_enum_translation_table_entry_t edge_type_sw_tt[] = { - {"simple_sw", IGRAPH_SIMPLE_SW}, - {"loops_sw", IGRAPH_LOOPS_SW}, - {"multi_sw", IGRAPH_MULTI_SW}, - {"both_sw", IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW}, + {"simple", IGRAPH_SIMPLE_SW}, + {"loops", IGRAPH_LOOPS_SW}, + {"multi", IGRAPH_MULTI_SW}, + {"all", IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW}, {0,0} }; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 5bf29bc19..e45127ed7 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2801,14 +2801,15 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, igraph_vector_t outdeg, indeg; igraph_vector_t *indegp = 0; - igraph_edge_type_sw_t allowed_edge_types; - igraph_realize_degseq_t method; - PyObject *outdeg_o, *indeg_o, *edge_types_o, *method_o; + igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; + igraph_realize_degseq_t method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; + PyObject *outdeg_o, *indeg_o = Py_None; + PyObject *edge_types_o = Py_None, *method_o = Py_None; igraphmodule_GraphObject *self; igraph_t g; - static char *kwlist[] = { "outdeg", "indeg", "allowed_edge_types", "method", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOOO", kwlist, + static char *kwlist[] = { "out", "in_", "allowed_edge_types", "method", NULL }; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", kwlist, &outdeg_o, &indeg_o, &edge_types_o, &method_o)) return NULL; @@ -12668,22 +12669,29 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {"Realize_Degree_Sequence", (PyCFunction) igraphmodule_Graph_Realize_Degree_Sequence, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Realize_Degree_Sequence(outdeg, indeg, allowed_edge_types, method)\n--\n\n" - "Generates a graph from a degree sequence.\n\n" - "@param outdeg: the degree sequence of an undirected graph (if indeg=None), " - "or the out-degree sequence of a directed graph.\n" - "@param indeg: None to generate an undirected graph, the in-degree sequence " - "to generate a directed graph.\n" - "@param allowed_edge_types: for directed graphs, only 'simple_sw' is currently \n" - "implemented. Possible values for undirected graphs are\n" - " - 'simple_sw: simple graphs (no self-loops, no multi-edges)\n" - " - 'loops_sw': single self-loops allowed, but not multi-edges \n" - " - 'multi_sw': multi-edges allowed, but not self-loops \n" - " - 'both_sw': multi-edges and self-loops allowed \n" - "@param method: possible values are\n" - " - 'smallest': The vertex with smallest remaining degree first.\n" - " - 'largest': The vertex with the largest remaining degree first.\n" - " - 'index': The vertices are selected in order of their index.\n" + "Realize_Degree_Sequence(out, in_=None, allowed_edge_types=\"simple\", method=\"smallest\")\n--\n\n" + "Generates a graph from a degree sequence.\n" + "\n" + "@param outdeg: the degree sequence of an undirected graph (if indeg=None),\n" + " or the out-degree sequence of a directed graph.\n" + "@param indeg: None to generate an undirected graph, the in-degree sequence\n" + " to generate a directed graph.\n" + "@param allowed_edge_types: controls whether loops or multi-edges are allowed\n" + " during the generation process. Note that not all combinations are supported\n" + " for all types of graphs; an exception will be raised for unsupported\n" + " combinations. Possible values are:\n" + "\n" + " - C{simple}: simple graphs (no self-loops, no multi-edges)\n" + " - C{loops}: single self-loops allowed, but not multi-edges\n" + " - C{multi}: multi-edges allowed, but not self-loops\n" + " - C{all}: multi-edges and self-loops allowed\n" + "\n" + "@param method: controls how the vertices are selected during the generation\n" + " process. Possible values are:\n" + "\n" + " - C{smallest}: The vertex with smallest remaining degree first.\n" + " - C{largest}: The vertex with the largest remaining degree first.\n" + " - C{index}: The vertices are selected in order of their index.\n" }, // interface to igraph_ring diff --git a/tests/test_generators.py b/tests/test_generators.py index 49d515df9..77ce8023b 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1,5 +1,6 @@ import unittest -from igraph import * + +from igraph import Graph, InternalError try: @@ -137,51 +138,69 @@ def testLCF(self): def testRealizeDegreeSequence(self): # Test case insensitivity of options too g = Graph.Realize_Degree_Sequence( - [1, 1], None, "simple_SW", "smallest", + [1, 1], None, "simPLE", "smallest", ) self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == [1, 1]) # Not implemented, should fail - self.assertRaises(NotImplementedError, Graph.Realize_Degree_Sequence, - [1, 1], None, "loops_sw", "largest") + self.assertRaises( + NotImplementedError, Graph.Realize_Degree_Sequence, + [1, 1], None, "loops", "largest" + ) g = Graph.Realize_Degree_Sequence( - [1, 1], None, "both_sw", "largest", + [1, 1], None, "all", "largest", ) self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == [1, 1]) g = Graph.Realize_Degree_Sequence( - [1, 1], None, "multi_sw", "index", + [1, 1], None, "multi", "index", ) self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == [1, 1]) g = Graph.Realize_Degree_Sequence( - [1, 1], [1, 1], "simple_sw", "largest", + [1, 1], [1, 1], "simple", "largest", ) self.assertTrue(g.is_directed()) + self.assertTrue(g.indegree() == [1, 1]) + self.assertTrue(g.outdegree() == [1, 1]) # Not implemented, should fail - self.assertRaises(NotImplementedError, Graph.Realize_Degree_Sequence, - [1, 1], [1, 1], "multi_sw", "largest") - - self.assertRaises(ValueError, Graph.Realize_Degree_Sequence, - [1, 1], [1, 1], "should_fail", "index") - self.assertRaises(ValueError, Graph.Realize_Degree_Sequence, - [1, 1], [1, 1], "multi_sw", "should_fail") + self.assertRaises( + NotImplementedError, Graph.Realize_Degree_Sequence, + [1, 1], [1, 1], "multi", "largest" + ) + self.assertRaises( + ValueError, Graph.Realize_Degree_Sequence, + [1, 1], [1, 1], "should_fail", "index" + ) + self.assertRaises( + ValueError, Graph.Realize_Degree_Sequence, + [1, 1], [1, 1], "multi", "should_fail" + ) + # Degree sequence of Zachary karate club, using optional arguments + zachary = Graph.Famous("zachary") + degrees = zachary.degree() + g = Graph.Realize_Degree_Sequence(degrees) + self.assertFalse(g.is_directed()) + self.assertTrue(g.degree() == degrees) def testKautz(self): g = Graph.Kautz(2, 2) - deg_in = g.degree(mode=IN) - deg_out = g.degree(mode=OUT) + deg_in = g.degree(mode="in") + deg_out = g.degree(mode="out") # This is not a proper test, but should spot most errors self.assertTrue(g.is_directed() and deg_in == [2] * 12 and deg_out == [2] * 12) def testDeBruijn(self): g = Graph.De_Bruijn(2, 3) - deg_in = g.degree(mode=IN, loops=True) - deg_out = g.degree(mode=OUT, loops=True) + deg_in = g.degree(mode="in", loops=True) + deg_out = g.degree(mode="out", loops=True) # This is not a proper test, but should spot most errors self.assertTrue(g.is_directed() and deg_in == [2] * 8 and deg_out == [2] * 8) From db032acbcfcfb7b21ab4806e1e1da6853ae4ac50 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 19 Apr 2021 13:51:59 +0200 Subject: [PATCH 0262/1681] chore: bumped igraph to 0.9.2 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 213abc889..7cf14dd11 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 213abc889d360775dfd0c11a406e859827c6dec8 +Subproject commit 7cf14dd11cd1784f6dc12d4e59464350f8f66608 From 7bc508692ed9f5dbb8d567c9d1767ccddcf70d84 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 19 Apr 2021 13:58:37 +0200 Subject: [PATCH 0263/1681] doc: mention that Realize_Degree_Sequence uses a Havel-Hakimi-style generation process --- src/_igraph/graphobject.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e45127ed7..722fb6d50 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -12672,6 +12672,13 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Realize_Degree_Sequence(out, in_=None, allowed_edge_types=\"simple\", method=\"smallest\")\n--\n\n" "Generates a graph from a degree sequence.\n" "\n" + "This method implements a Havel-Hakimi style graph construction from a given\n" + "degree sequence. In each step, the algorithm picks two vertices in a\n" + "deterministic manner and connects them. The way the vertices are picked is\n" + "defined by the C{method} parameter. The allowed edge types (i.e. whether\n" + "multiple or loop edges are allowed) are specified in the C{allowed_edge_types}\n" + "parameter.\n" + "\n" "@param outdeg: the degree sequence of an undirected graph (if indeg=None),\n" " or the out-degree sequence of a directed graph.\n" "@param indeg: None to generate an undirected graph, the in-degree sequence\n" From 6d4059e0569a3dc7625e5c21c2e02db974b9b66e Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 09:37:32 +1000 Subject: [PATCH 0264/1681] First version, passes tests --- doc/source/generation.rst | 1 + src/_igraph/graphobject.c | 66 +++++++++++++++++++++++++++++++++++++- src/_igraph/graphobject.h | 1 + src/_igraph/igraphmodule.c | 3 ++ src/igraph/__init__.py | 2 ++ tests/test_games.py | 9 ++++++ 6 files changed, 81 insertions(+), 1 deletion(-) diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 4659406a1..d41da27d4 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -148,6 +148,7 @@ Stochastic graphs can be created according to several different models or games: - Erdos-Renyi: :meth:`Graph.Erdos_Renyi` - Watts-Strogatz :meth:`Graph.Watts_Strogatz` - stochastic block model :meth:`Graph.SBM` + - random tree :meth:`Graph.Tree_Game` - forest fire game :meth:`Graph.Forest_Fire` - random geometric graph :meth:`Graph.GRG` - growing :meth:`Graph.Growing_Random` diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 722fb6d50..c118c3b62 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -674,7 +674,7 @@ PyObject *igraphmodule_Graph_delete_edges(igraphmodule_GraphObject * self, } /********************************************************************** - * tructural properties * + * structural properties * **********************************************************************/ /** \ingroup python_interface_graph @@ -3462,6 +3462,60 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, return (PyObject *) self; } +/** \ingroup python_interface_graph + * \brief Generates a random tree using one of a few methods. + * + * This method has three parameters: + * - n is the number of nodes in the tree. + * - directed is a bool that specifies if the edges should be directed. If so, they + * point away from the root. + * - method is one of: + * - igraph.RANDOM_TREE_PRUFER (Prufer, directed currently unsupported) + * - igraph.RANDOM_TREE_LERW (loop-erased random walk) + * + * \return a reference to the newly generated Python igraph object + * \sa igraph_tree + */ +PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, + PyObject * args, PyObject * kwds) +{ + long int n, directed_o, tree_method_o; + igraphmodule_GraphObject *self; + igraph_t g; + + static char *kwlist[] = { "n", "directed", "method", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "lll", kwlist, + &n, &directed_o, &tree_method_o)) + return NULL; + + if (n < 0) { + PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); + return NULL; + } + + switch(tree_method_o) { + case IGRAPH_RANDOM_TREE_PRUFER: + case IGRAPH_RANDOM_TREE_LERW: + break; + default: + PyErr_SetString(PyExc_ValueError, + "Tree method must be igraph.RANDOM_TREE_PRUFER or igraph.RANDOM_TREE_LERW"); + return NULL; + } + + /* The C function takes care of the unsupported case (Prufer/directed) */ + if (igraph_tree_game(&g, (igraph_integer_t) n, (igraph_bool_t) directed_o, + tree_method_o)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + CREATE_GRAPH_FROM_TYPE(self, g, type); + + return (PyObject *) self; +} + /** \ingroup python_interface_graph * \brief Generates a graph based on the Watts-Strogatz model * \return a reference to the newly generated Python igraph object @@ -12821,6 +12875,16 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@param cls: the isomorphism class\n" "@param directed: whether the graph should be directed.\n"}, + /* interface to igraph_tree_game */ + {"Tree_Game", (PyCFunction) igraphmodule_Graph_Tree_Game, + METH_VARARGS | METH_CLASS | METH_KEYWORDS, + "Tree_Game(n, directed, method)\n--\n\n" + "Generates a random tree.\n\n" + "@param n: the number of vertices in the tree\n" + "@param directed: whether the graph should be directed.\n" + "@param method: one of RANDOM_TREE_PRUFER or RANDOM_TREE_LERW.\n" + }, + /* interface to igraph_watts_strogatz_game */ {"Watts_Strogatz", (PyCFunction) igraphmodule_Graph_Watts_Strogatz, METH_VARARGS | METH_CLASS | METH_KEYWORDS, diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 3ab836942..d9d3c1fc8 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -99,6 +99,7 @@ PyObject* igraphmodule_Graph_Ring(PyTypeObject *type, PyObject *args, PyObject * PyObject* igraphmodule_Graph_SBM(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Star(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Tree(PyTypeObject *type, PyObject *args, PyObject *kwds); +PyObject* igraphmodule_Graph_Tree_Game(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_Watts_Strogatz(PyTypeObject *type, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_is_connected(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index ea3661c3f..77d087dcc 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -904,6 +904,9 @@ PyObject* PyInit__igraph(void) PyModule_AddIntConstant(m, "LOOPS_SW", IGRAPH_LOOPS_SW); PyModule_AddIntConstant(m, "MULTI_SW", IGRAPH_MULTI_SW); + PyModule_AddIntConstant(m, "RANDOM_TREE_PRUFER", IGRAPH_RANDOM_TREE_PRUFER); + PyModule_AddIntConstant(m, "RANDOM_TREE_LERW", IGRAPH_RANDOM_TREE_LERW); + /* More useful constants */ { const char* version; diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 547c56a85..32417afa3 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -50,6 +50,8 @@ IN, InternalError, OUT, + RANDOM_TREE_PRUFER, + RANDOM_TREE_LERW, REWIRING_SIMPLE, REWIRING_SIMPLE_LOOPS, STAR_IN, diff --git a/tests/test_games.py b/tests/test_games.py index 0af5b9aa2..1d50ef672 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -45,6 +45,15 @@ def testAsymmetricPreference(self): g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]]) self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 1) + def testTreeGame(self): + # Prufer algorithm + g = Graph.Tree_Game(10, False, RANDOM_TREE_PRUFER) + self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) + + # LERW algorithm + g = Graph.Tree_Game(10, False, RANDOM_TREE_LERW) + self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) + def testWattsStrogatz(self): g = Graph.Watts_Strogatz(1, 20, 1, 0.2) self.assertTrue(isinstance(g, Graph) and g.vcount() == 20 and g.ecount() == 20) From 69e3e1f049729936a58cfcdc24cff8688253c465 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 19 Apr 2021 09:48:41 +1000 Subject: [PATCH 0265/1681] One more test that fails if Prufer + directed is chosen --- tests/test_games.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_games.py b/tests/test_games.py index 1d50ef672..a920d42a9 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -50,6 +50,9 @@ def testTreeGame(self): g = Graph.Tree_Game(10, False, RANDOM_TREE_PRUFER) self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) + # Prufer with directed (should fail) + self.assertRaises(InternalError, Graph.Tree_Game, 10, True, RANDOM_TREE_PRUFER) + # LERW algorithm g = Graph.Tree_Game(10, False, RANDOM_TREE_LERW) self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) From 61c3428b8cb317a5690bd46fd56a33887ea2879f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 20 Apr 2021 08:11:19 +1000 Subject: [PATCH 0266/1681] Rebase on top of strict conversion function --- src/_igraph/convert.c | 15 +++++++++++++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 30 ++++++++++++++---------------- src/_igraph/igraphmodule.c | 3 --- src/igraph/__init__.py | 2 -- tests/test_games.py | 6 +++--- 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 52adff732..48277893f 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -3108,3 +3108,18 @@ int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq return igraphmodule_PyObject_to_enum_strict(o, realize_degseq_tt, (int*)result); } + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_random_tree_t + */ +int igraphmodule_PyObject_to_random_tree_t(PyObject *o, igraph_random_tree_t *result) { + static igraphmodule_enum_translation_table_entry_t random_tree_tt[] = { + {"prufer", IGRAPH_RANDOM_TREE_PRUFER}, + {"lerw", IGRAPH_RANDOM_TREE_LERW}, + {0,0} + }; + + return igraphmodule_PyObject_to_enum_strict(o, random_tree_tt, (int*)result); +} + diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 24af08abf..19cd98500 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -67,6 +67,7 @@ int igraphmodule_PyObject_to_neimode_t(PyObject *o, igraph_neimode_t *result); int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t *result); int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result); int igraphmodule_PyObject_to_realize_degseq_t(PyObject *o, igraph_realize_degseq_t *result); +int igraphmodule_PyObject_to_random_tree_t(PyObject *o, igraph_random_tree_t *result); int igraphmodule_PyObject_to_random_walk_stuck_t(PyObject *o, igraph_random_walk_stuck_t *result); int igraphmodule_PyObject_to_reciprocity_t(PyObject *o, igraph_reciprocity_t *result); int igraphmodule_PyObject_to_rewiring_t(PyObject *o, igraph_rewiring_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index c118c3b62..dddbf0fa9 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3470,8 +3470,9 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, * - directed is a bool that specifies if the edges should be directed. If so, they * point away from the root. * - method is one of: - * - igraph.RANDOM_TREE_PRUFER (Prufer, directed currently unsupported) - * - igraph.RANDOM_TREE_LERW (loop-erased random walk) + * - 'Prufer' aka sample Pruefer sequences and convert to trees. + * - 'lerw' aka loop-erased random walk on the complete graph to sample spanning + * trees. * * \return a reference to the newly generated Python igraph object * \sa igraph_tree @@ -3479,13 +3480,16 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long int n, directed_o, tree_method_o; + long int n; + PyObject *directed_o, *tree_method_o; + igraph_bool_t directed; + igraph_random_tree_t tree_method; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "directed", "method", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lll", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "lOO", kwlist, &n, &directed_o, &tree_method_o)) return NULL; @@ -3494,19 +3498,13 @@ PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, return NULL; } - switch(tree_method_o) { - case IGRAPH_RANDOM_TREE_PRUFER: - case IGRAPH_RANDOM_TREE_LERW: - break; - default: - PyErr_SetString(PyExc_ValueError, - "Tree method must be igraph.RANDOM_TREE_PRUFER or igraph.RANDOM_TREE_LERW"); + directed = PyObject_IsTrue(directed_o); + + if (igraphmodule_PyObject_to_random_tree_t(tree_method_o, &tree_method)) return NULL; - } - /* The C function takes care of the unsupported case (Prufer/directed) */ - if (igraph_tree_game(&g, (igraph_integer_t) n, (igraph_bool_t) directed_o, - tree_method_o)) { + if (igraph_tree_game(&g, (igraph_integer_t) n, directed, + tree_method)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -12882,7 +12880,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Generates a random tree.\n\n" "@param n: the number of vertices in the tree\n" "@param directed: whether the graph should be directed.\n" - "@param method: one of RANDOM_TREE_PRUFER or RANDOM_TREE_LERW.\n" + "@param method: one of 'Prufer' or 'lerw'.\n" }, /* interface to igraph_watts_strogatz_game */ diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 77d087dcc..ea3661c3f 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -904,9 +904,6 @@ PyObject* PyInit__igraph(void) PyModule_AddIntConstant(m, "LOOPS_SW", IGRAPH_LOOPS_SW); PyModule_AddIntConstant(m, "MULTI_SW", IGRAPH_MULTI_SW); - PyModule_AddIntConstant(m, "RANDOM_TREE_PRUFER", IGRAPH_RANDOM_TREE_PRUFER); - PyModule_AddIntConstant(m, "RANDOM_TREE_LERW", IGRAPH_RANDOM_TREE_LERW); - /* More useful constants */ { const char* version; diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 32417afa3..547c56a85 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -50,8 +50,6 @@ IN, InternalError, OUT, - RANDOM_TREE_PRUFER, - RANDOM_TREE_LERW, REWIRING_SIMPLE, REWIRING_SIMPLE_LOOPS, STAR_IN, diff --git a/tests/test_games.py b/tests/test_games.py index a920d42a9..40a4d50f7 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -47,14 +47,14 @@ def testAsymmetricPreference(self): def testTreeGame(self): # Prufer algorithm - g = Graph.Tree_Game(10, False, RANDOM_TREE_PRUFER) + g = Graph.Tree_Game(10, False, "Prufer") self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) # Prufer with directed (should fail) - self.assertRaises(InternalError, Graph.Tree_Game, 10, True, RANDOM_TREE_PRUFER) + self.assertRaises(InternalError, Graph.Tree_Game, 10, True, "Prufer") # LERW algorithm - g = Graph.Tree_Game(10, False, RANDOM_TREE_LERW) + g = Graph.Tree_Game(10, False, "lerw") self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) def testWattsStrogatz(self): From f91bfd229b09f1ad6235f9b9a8fffe851d97d88e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 20 Apr 2021 15:52:26 +0200 Subject: [PATCH 0267/1681] feat: added Graph.is_tree() --- src/_igraph/graphobject.c | 88 ++++++++++++++++++++++++++++++++------- tests/test_structural.py | 40 ++++++++++++++++++ 2 files changed, 113 insertions(+), 15 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index dddbf0fa9..8c348dde1 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -536,6 +536,40 @@ PyObject *igraphmodule_Graph_is_simple(igraphmodule_GraphObject *self) { Py_RETURN_FALSE; } + +/** \ingroup python_interface_graph + * \brief Determines whether a graph is a (directed or undirected) tree + * \sa igraph_is_tree + */ +PyObject *igraphmodule_Graph_is_tree(igraphmodule_GraphObject * self, + PyObject * args, PyObject * kwds) +{ + static char *kwlist[] = { "mode", NULL }; + PyObject *mode_o = Py_None; + igraph_neimode_t mode = IGRAPH_OUT; + igraph_bool_t result; + int retval; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { + return NULL; + } + + if (igraph_is_tree(&self->g, &result, /* root = */ 0, mode)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (result) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + /** \ingroup python_interface_graph * \brief Adds vertices to an \c igraph.Graph * \return the extended \c igraph.Graph object @@ -674,7 +708,7 @@ PyObject *igraphmodule_Graph_delete_edges(igraphmodule_GraphObject * self, } /********************************************************************** - * structural properties * + * Structural properties * **********************************************************************/ /** \ingroup python_interface_graph @@ -3478,18 +3512,18 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, * \sa igraph_tree */ PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, - PyObject * args, PyObject * kwds) + PyObject * args, PyObject * kwds) { long int n; - PyObject *directed_o, *tree_method_o; + PyObject *directed_o = Py_False, *tree_method_o = Py_None; igraph_bool_t directed; - igraph_random_tree_t tree_method; + igraph_random_tree_t tree_method = IGRAPH_RANDOM_TREE_LERW; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "directed", "method", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|OO", kwlist, &n, &directed_o, &tree_method_o)) return NULL; @@ -12124,7 +12158,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: C{True} if it is directed, C{False} otherwise.\n" "@rtype: boolean"}, - // interface to igraph_is_simple + /* interface to igraph_is_simple */ {"is_simple", (PyCFunction) igraphmodule_Graph_is_simple, METH_NOARGS, "is_simple()\n--\n\n" @@ -12132,6 +12166,22 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "@return: C{True} if it is simple, C{False} otherwise.\n" "@rtype: boolean"}, + /* interface to igraph_is_tree */ + {"is_tree", (PyCFunction) igraphmodule_Graph_is_tree, + METH_VARARGS | METH_KEYWORDS, + "is_tree(mode=\"out\")\n--\n\n" + "Checks whether the graph is a (directed or undirected) tree graph.\n\n" + "For directed trees, the function may require that the edges are oriented\n" + "outwards from the root or inwards to the root, depending on the value\n" + "of the C{mode} argument.\n\n" + "@param mode: for directed graphs, specifies how the edge directions\n" + " should be taken into account. C{\"all\"} means that the edge directions\n" + " must be ignored, C{\"out\"} means that the edges must be oriented away\n" + " from the root, C{\"in\"} means that the edges must be oriented\n" + " towards the root. Ignored for undirected graphs.\n" + "@return: C{True} if the graph is a tree, C{False} otherwise.\n" + "@rtype: boolean"}, + /* interface to igraph_add_vertices */ {"add_vertices", (PyCFunction) igraphmodule_Graph_add_vertices, METH_VARARGS, @@ -12740,10 +12790,10 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " for all types of graphs; an exception will be raised for unsupported\n" " combinations. Possible values are:\n" "\n" - " - C{simple}: simple graphs (no self-loops, no multi-edges)\n" - " - C{loops}: single self-loops allowed, but not multi-edges\n" - " - C{multi}: multi-edges allowed, but not self-loops\n" - " - C{all}: multi-edges and self-loops allowed\n" + " - C{\"simple\"}: simple graphs (no self-loops, no multi-edges)\n" + " - C{\"loops\"}: single self-loops allowed, but not multi-edges\n" + " - C{\"multi\"}: multi-edges allowed, but not self-loops\n" + " - C{\"all\"}: multi-edges and self-loops allowed\n" "\n" "@param method: controls how the vertices are selected during the generation\n" " process. Possible values are:\n" @@ -12876,12 +12926,20 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_tree_game */ {"Tree_Game", (PyCFunction) igraphmodule_Graph_Tree_Game, METH_VARARGS | METH_CLASS | METH_KEYWORDS, - "Tree_Game(n, directed, method)\n--\n\n" - "Generates a random tree.\n\n" + "Tree_Game(n, directed=False, method=\"lerw\")\n--\n\n" + "Generates a random tree by sampling uniformly from the set of labelled\n" + "trees with a given number of nodes.\n\n" "@param n: the number of vertices in the tree\n" - "@param directed: whether the graph should be directed.\n" - "@param method: one of 'Prufer' or 'lerw'.\n" - }, + "@param directed: whether the graph should be directed\n" + "@param method: the generation method to be used. One of the following:\n" + " \n" + " - C{\"prufer\"} -- samples Prufer sequences uniformly, then converts\n" + " them to trees\n" + " - C{\"lerw\"} -- performs a loop-erased random walk on the complete\n" + " graph to uniformly sample its spanning trees (Wilson's algorithm).\n" + " This is the default choice as it supports both directed and\n" + " undirected graphs.\n" + }, /* interface to igraph_watts_strogatz_game */ {"Watts_Strogatz", (PyCFunction) igraphmodule_Graph_Watts_Strogatz, diff --git a/tests/test_structural.py b/tests/test_structural.py index 148cf8539..545733d1e 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -527,6 +527,46 @@ def testIsDAG(self): g = Graph.Ring(10, directed=True, mutual=False) self.assertFalse(g.is_dag()) + def testIsTree(self): + g = Graph() + self.assertFalse(g.is_tree()) + + g = Graph(directed=True) + self.assertFalse(g.is_tree()) + + g = Graph(1) + self.assertTrue(g.is_tree()) + + g = Graph(1, directed=True) + self.assertTrue(g.is_tree() and g.is_tree("out") and g.is_tree("in") and g.is_tree("all")) + + g = Graph(5, [(0, 1), (1, 2), (1, 3), (3, 4)]) + self.assertTrue(g.is_tree()) + + g = Graph(5, [(0, 1), (1, 2), (1, 3), (3, 4)], directed=True) + self.assertTrue(g.is_tree()) + self.assertTrue(g.is_tree("out")) + self.assertFalse(g.is_tree("in")) + self.assertTrue(g.is_tree("all")) + + g = Graph(5, [(0, 1), (1, 2), (3, 1), (3, 4)], directed=True) + self.assertFalse(g.is_tree()) + self.assertFalse(g.is_tree("in")) + self.assertFalse(g.is_tree("out")) + self.assertTrue(g.is_tree("all")) + + g = Graph(6, [(0, 4), (1, 5), (2, 1), (3, 1), (4, 3)], directed=True) + self.assertFalse(g.is_tree()) + self.assertTrue(g.is_tree("in")) + self.assertFalse(g.is_tree("out")) + self.assertTrue(g.is_tree("all")) + + g = Graph.Ring(10) + self.assertFalse( + g.is_tree() or g.is_tree("in") or g.is_tree("out") or + g.is_tree("all") + ) + def testLineGraph(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) el = g.linegraph().get_edgelist() From ee2f7be35812df608142c36a2832cba7c818c2dc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 20 Apr 2021 15:52:47 +0200 Subject: [PATCH 0268/1681] test: extended Tree_Game() tests with checks that validate whether the graph is indeed a tree --- tests/test_games.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_games.py b/tests/test_games.py index 40a4d50f7..5b1a35ed1 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -49,6 +49,8 @@ def testTreeGame(self): # Prufer algorithm g = Graph.Tree_Game(10, False, "Prufer") self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_tree()) # Prufer with directed (should fail) self.assertRaises(InternalError, Graph.Tree_Game, 10, True, "Prufer") @@ -56,6 +58,20 @@ def testTreeGame(self): # LERW algorithm g = Graph.Tree_Game(10, False, "lerw") self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_tree()) + + # Omitting the algorithm should default to LERW + g = Graph.Tree_Game(10, directed=True) + self.assertTrue(isinstance(g, Graph) and g.vcount() == 10) + self.assertTrue(g.is_directed()) + self.assertTrue(g.is_tree()) + + # Omitting the directed argument should use undirected graphs + g = Graph.Tree_Game(42, method="Prufer") + self.assertTrue(isinstance(g, Graph) and g.vcount() == 42) + self.assertFalse(g.is_directed()) + self.assertTrue(g.is_tree()) def testWattsStrogatz(self): g = Graph.Watts_Strogatz(1, 20, 1, 0.2) From 4578205f1a86cec1243ab70dca905ba6315b21f4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 20 Apr 2021 15:56:11 +0200 Subject: [PATCH 0269/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8fea258e..613abf7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # igraph Python interface changelog +## Unreleased + +### Added + +* Added `Graph.is_tree()` to test whether a graph is a tree. + +* Added `Graph.Realize_Degree_Sequence()` to construct a graph that realizes a + given degree sequence, using a deterministic (Havel-Hakimi-style) algorithm. + +* Added `Graph.Tree_Game()` to generate random trees with uniform sampling. + ## 0.9.1 ### Changed From fab33f15ebf094399123beed0b0286bf31a63f8d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 1 May 2021 15:06:24 +0200 Subject: [PATCH 0270/1681] feat: trying to support Apple Silicon (arm64) wheels via cibuildwheel --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7cd1e6844..2e2683e2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,6 +64,7 @@ jobs: - name: Build wheels uses: joerick/cibuildwheel@v1.10.0 env: + CIBW_ARCHS_MACOS: "x86_64 arm64" CIBW_BEFORE_BUILD: "python setup.py build_c_core" - uses: actions/upload-artifact@v2 From ea91cc99b9a77d61daab4469daec0cae7dce89bc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 1 May 2021 15:52:38 +0200 Subject: [PATCH 0271/1681] fix: handle build failures in the C core of igraph properly --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index ffa7dedac..37c34702f 100644 --- a/setup.py +++ b/setup.py @@ -571,6 +571,11 @@ def compile_igraph_from_vendor_source(self): finally: os.chdir(cwd) + if libraries is False: + print("Build failed for the C core of igraph.") + print("") + return False + igraph_builder.create_build_config_file(install_folder, libraries) self.use_vendored_igraph() From c789bebec1705dd420aa158f206dd192142719f8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 1 May 2021 16:14:59 +0200 Subject: [PATCH 0272/1681] exit when building the C core fails --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 37c34702f..5f2e4a5b0 100644 --- a/setup.py +++ b/setup.py @@ -574,7 +574,7 @@ def compile_igraph_from_vendor_source(self): if libraries is False: print("Build failed for the C core of igraph.") print("") - return False + sys.exit(1) igraph_builder.create_build_config_file(install_folder, libraries) From fce6e1a6422297426b27cc79fefb95f8cfed1b5b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 1 May 2021 16:17:02 +0200 Subject: [PATCH 0273/1681] fix: remove unused variable --- src/_igraph/graphobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8c348dde1..6dc02cfff 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -548,7 +548,6 @@ PyObject *igraphmodule_Graph_is_tree(igraphmodule_GraphObject * self, PyObject *mode_o = Py_None; igraph_neimode_t mode = IGRAPH_OUT; igraph_bool_t result; - int retval; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) { return NULL; From 921ee3e2b4931027e58aa67dcbf70bd0a69628de Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 1 May 2021 16:19:00 +0200 Subject: [PATCH 0274/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 7cf14dd11..9fdd510c2 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 7cf14dd11cd1784f6dc12d4e59464350f8f66608 +Subproject commit 9fdd510c257ccbc0919be2637de1f16d3bff57da From 2077777ae38a0feff9b037552af42e48708e4e18 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 1 May 2021 16:19:15 +0200 Subject: [PATCH 0275/1681] style: minor formatting in src/_igraph/attributes.c --- src/_igraph/attributes.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 191b9124b..7c4eb8749 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -820,9 +820,11 @@ static PyObject* igraphmodule_i_ac_func(PyObject* values, PyObject *res, *list, *item; res = PyList_New(len); + for (i = 0; i < len; i++) { igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; long int j, n = igraph_vector_size(v); + list = PyList_New(n); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -831,10 +833,12 @@ static PyObject* igraphmodule_i_ac_func(PyObject* values, } item = PyObject_CallFunctionObjArgs(func, list, 0); Py_DECREF(list); + if (item == 0) { Py_DECREF(res); return 0; } + PyList_SET_ITEM(res, i, item); /* reference to item stolen */ } @@ -1150,6 +1154,7 @@ static int igraphmodule_i_attribute_combine_dicts(PyObject *dict, Py_ssize_t pos; igraph_attribute_combination_record_t* todo; Py_ssize_t i, n; + if (!PyDict_Check(dict) || !PyDict_Check(newdict)) return 1; /* Allocate memory for the attribute_combination_records */ @@ -1282,8 +1287,8 @@ static int igraphmodule_i_attribute_combine_vertices(const igraph_t *graph, int result; /* Get the attribute dicts */ - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; - newdict=ATTR_STRUCT_DICT(newgraph)[ATTRHASH_IDX_VERTEX]; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; + newdict = ATTR_STRUCT_DICT(newgraph)[ATTRHASH_IDX_VERTEX]; /* Combine the attribute dicts */ result = igraphmodule_i_attribute_combine_dicts(dict, newdict, From 4632c99e34756fe771472ea16efd780a66c81ca2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 4 May 2021 18:20:53 +0200 Subject: [PATCH 0276/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 9fdd510c2..ecbe39a4e 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 9fdd510c257ccbc0919be2637de1f16d3bff57da +Subproject commit ecbe39a4eb8d4319633e3e364fd72fe7fe9ae109 From 1d3a465232c88f8fc218f81a80aa38417ae6080e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 4 May 2021 21:01:02 +0200 Subject: [PATCH 0277/1681] ci: bump cibuildwheel action to v1.11.0 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e2683e2b..2c05589e4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,7 +119,7 @@ jobs: shell: cmd - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v1.11.0 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From a310852c554b223aa23532d9fba502cb536f81a2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 4 May 2021 23:08:12 +0200 Subject: [PATCH 0278/1681] feat: updated minimal Docker image file; we don't need to compile igraph in Docker now --- docker/minimal/Dockerfile | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/docker/minimal/Dockerfile b/docker/minimal/Dockerfile index 7a598ebb4..d389de1d5 100644 --- a/docker/minimal/Dockerfile +++ b/docker/minimal/Dockerfile @@ -1,25 +1,8 @@ -FROM alpine:latest AS builder +FROM python:latest MAINTAINER Tamas Nepusz LABEL Description="Simple Docker image that contains a pre-compiled version of igraph's Python interface" -RUN apk add --no-cache --update \ - make gcc g++ libstdc++ git python3-dev libxslt-dev libxml2-dev libc-dev \ - libffi-dev zlib-dev py-pip libxml2 zlib libtool autoconf automake \ - flex bison \ - && rm -rf /var/cache/apk/* +RUN pip3 install python-igraph cairocffi -RUN pip3 install git+https://github.com/igraph/python-igraph -RUN pip3 install cairocffi - -FROM alpine:latest - -RUN apk add --no-cache --update \ - python3 libstdc++ libxml2 libxslt zlib cairo \ - && rm -rf /var/cache/apk/* - -COPY --from=builder /usr/lib/python3.7/site-packages/cairocffi /usr/lib/python3.7/site-packages/cairocffi -COPY --from=builder /usr/lib/python3.7/site-packages/igraph /usr/lib/python3.7/site-packages/igraph -COPY --from=builder /usr/bin/igraph /usr/bin - -CMD /usr/bin/igraph +CMD /usr/local/bin/igraph From 2054f463ec7454e01d17ff774d7ffc81b808803b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 5 May 2021 14:25:30 +0200 Subject: [PATCH 0279/1681] fix: removed unneeded manylinux.docker file as we now use cibuildwheel in Github Actions --- .gitignore | 1 - docker/manylinux.docker | 28 ---------------------------- 2 files changed, 29 deletions(-) delete mode 100644 docker/manylinux.docker diff --git a/.gitignore b/.gitignore index a285acca8..918256eeb 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ src/igraph/*.so .tox .venv/ .vscode/ -docker/wheelhouse vendor/build/ vendor/install/ Pipfile diff --git a/docker/manylinux.docker b/docker/manylinux.docker deleted file mode 100644 index 77d8ce18e..000000000 --- a/docker/manylinux.docker +++ /dev/null @@ -1,28 +0,0 @@ -# manylinux.docker -- dockerfile for generating manylinux wheel - -FROM quay.io/pypa/manylinux2010_x86_64 -MAINTAINER awinter.public@gmail.com - -RUN echo "multilib_policy=best" >> /etc/yum.conf -RUN yum install -y flex bison zlib-devel libxml2-devel -# kill static libxml2 as per http://lists.gnu.org/archive/html/igraph-help/2014-05/msg00045.html -# RUN mv /usr/lib64/libxml2.a /usr/lib64/libxml2.a.org - -COPY ./ /python-igraph/ -RUN mkdir /wheelhouse -WORKDIR /python-igraph - -# Do not create bytecode files -- this will make setup.py try to compile the C -# extension first, without compiling the C core. This should be fixed in -# setup.py later, though. -ENV PYTHONDONTWRITEBYTECODE=1 - -RUN /bin/bash -c "for PYBIN in /opt/python/*/bin; do \ - echo \$PYBIN; \ - \$PYBIN/pip wheel . -w /wheelhouse; \ -done" - -# Bundle external shared libraries into the wheels -RUN /bin/bash -c "for whl in /wheelhouse/*.whl; do \ - auditwheel repair \$whl -w /wheelhouse/; \ -done" From 2baed119aac8355d561876cbd383f34c5004ee3a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 5 May 2021 18:19:04 +0200 Subject: [PATCH 0280/1681] feat: added more modes for Graph.to_directed(), closes #376 --- CHANGELOG.md | 6 +++++ src/_igraph/convert.c | 25 +++++++++++++++++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 50 +++++++++++++++++++++++++++----------- tests/test_conversion.py | 51 +++++++++++++++++++++++++++++++++++++-- 5 files changed, 117 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 613abf7bc..7d7a9f11c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ * Added `Graph.Tree_Game()` to generate random trees with uniform sampling. +* `Graph.to_directed()` now supports a `mode=...` keyword argument. + +### Deprecated + +* `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. + ## 0.9.1 ### Changed diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 48277893f..b9f0ad18d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -641,6 +641,31 @@ int igraphmodule_PyObject_to_subgraph_implementation_t(PyObject *o, return igraphmodule_PyObject_to_enum(o, subgraph_impl_tt, (int*)result); } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python object to an igraph \c igraph_to_directed_t + */ +int igraphmodule_PyObject_to_to_directed_t(PyObject *o, + igraph_to_directed_t *result) { + static igraphmodule_enum_translation_table_entry_t to_directed_tt[] = { + {"acyclic", IGRAPH_TO_DIRECTED_ACYCLIC}, + {"arbitrary", IGRAPH_TO_DIRECTED_ARBITRARY}, + {"mutual", IGRAPH_TO_DIRECTED_MUTUAL}, + {"random", IGRAPH_TO_DIRECTED_RANDOM}, + {0,0} + }; + + if (o == Py_True) { + *result = IGRAPH_TO_DIRECTED_MUTUAL; + return 0; + } else if (o == Py_False) { + *result = IGRAPH_TO_DIRECTED_ARBITRARY; + return 0; + } + + return igraphmodule_PyObject_to_enum(o, to_directed_tt, (int*)result); +} + /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_to_undirected_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 19cd98500..bff912b83 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -75,6 +75,7 @@ int igraphmodule_PyObject_to_spinglass_implementation_t(PyObject *o, igraph_spin int igraphmodule_PyObject_to_spincomm_update_t(PyObject *o, igraph_spincomm_update_t *result); int igraphmodule_PyObject_to_star_mode_t(PyObject *o, igraph_star_mode_t *result); int igraphmodule_PyObject_to_subgraph_implementation_t(PyObject *o, igraph_subgraph_implementation_t *result); +int igraphmodule_PyObject_to_to_directed_t(PyObject *o, igraph_to_directed_t *result); int igraphmodule_PyObject_to_to_undirected_t(PyObject *o, igraph_to_undirected_t *result); int igraphmodule_PyObject_to_transitivity_mode_t(PyObject *o, igraph_transitivity_mode_t *result); int igraphmodule_PyObject_to_tree_mode_t(PyObject *o, igraph_tree_mode_t *result); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6dc02cfff..cacb41592 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -608,9 +608,9 @@ PyObject *igraphmodule_Graph_delete_vertices(igraphmodule_GraphObject * self, /*Py_None also means all for now, but it is deprecated */ if (list == Py_None) { - PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is " - "deprecated since igraph 0.8.3, please use " - "Graph.delete_vertices() instead"); + PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is " + "deprecated since igraph 0.8.3, please use " + "Graph.delete_vertices() instead"); } /* this already converts no arguments and Py_None to all vertices */ @@ -7850,18 +7850,36 @@ PyObject *igraphmodule_Graph_to_undirected(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_to_directed(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *mutual = Py_True; + PyObject *mutual_o = Py_None; + PyObject *mode_o = Py_None; igraph_to_directed_t mode = IGRAPH_TO_DIRECTED_MUTUAL; - static char *kwlist[] = { "mutual", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mutual)) + static char *kwlist[] = { "mode", "mutual", NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &mutual_o)) return NULL; - mode = - (PyObject_IsTrue(mutual) ? IGRAPH_TO_DIRECTED_MUTUAL : - IGRAPH_TO_DIRECTED_ARBITRARY); + + if (mode_o == Py_None) { + /* mode argument omitted so we fall back to 'mutual' for sake of + * compatibility and print a warning */ + if (mutual_o == Py_None) { + /* mutual was not given either, so this is okay */ + mode = IGRAPH_TO_DIRECTED_MUTUAL; + } else { + mode = PyObject_IsTrue(mutual_o) ? IGRAPH_TO_DIRECTED_MUTUAL : IGRAPH_TO_DIRECTED_ARBITRARY; + PyErr_Warn(PyExc_DeprecationWarning, "The 'mutual' argument is deprecated since " + "igraph 0.9.3, please use mode=... instead"); + } + } else { + if (igraphmodule_PyObject_to_to_directed_t(mode_o, &mode)) { + return NULL; + } + } + if (igraph_to_directed(&self->g, mode)) { igraphmodule_handle_igraph_error(); return NULL; } + Py_RETURN_NONE; } @@ -14802,14 +14820,18 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "Internal function, undocumented.\n\n" "@see: Graph.get_incidence()\n\n"}, - // interface to igraph_to_directed + /* interface to igraph_to_directed */ {"to_directed", (PyCFunction) igraphmodule_Graph_to_directed, METH_VARARGS | METH_KEYWORDS, - "to_directed(mutual=True)\n--\n\n" + "to_directed(mode=\"mutual\")\n--\n\n" "Converts an undirected graph to directed.\n\n" - "@param mutual: C{True} if mutual directed edges should be\n" - " created for every undirected edge. If C{False}, a directed\n" - " edge with arbitrary direction is created.\n"}, + "@param mode: specifies how to convert undirected edges into\n" + " directed ones. C{True} or C{\"mutual\"} creates a mutual edge pair\n" + " for each undirected edge. C{False} or C{\"arbitrary\"} picks an\n" + " arbitrary (but deterministic) edge direction for each edge.\n" + " C{\"random\"} picks a random direction for each edge. C{\"acyclic\"}\n" + " picks the edge directions in a way that the resulting graph will be\n" + " acyclic if there were no self-loops in the original graph.\n" // interface to igraph_to_undirected {"to_undirected", (PyCFunction) igraphmodule_Graph_to_undirected, diff --git a/tests/test_conversion.py b/tests/test_conversion.py index 6c3860806..aac08af5a 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -1,5 +1,7 @@ +import random import unittest -from igraph import * + +from igraph import Graph, Matrix class DirectedUndirectedTests(unittest.TestCase): @@ -37,7 +39,7 @@ def testToUndirected(self): graph2.es["weight"] == [7, 3, 11] or graph2.es["weight"] == [3, 7, 11] ) - def testToDirected(self): + def testToDirectedNoModeArg(self): graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False) graph.to_directed() self.assertTrue(graph.is_directed()) @@ -47,6 +49,51 @@ def testToDirected(self): == [(0, 1), (0, 2), (1, 0), (2, 0), (2, 3), (2, 4), (3, 2), (4, 2)] ) + def testToDirectedMutual(self): + graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False) + graph.to_directed("mutual") + self.assertTrue(graph.is_directed()) + self.assertTrue(graph.vcount() == 5) + self.assertTrue( + sorted(graph.get_edgelist()) + == [(0, 1), (0, 2), (1, 0), (2, 0), (2, 3), (2, 4), (3, 2), (4, 2)] + ) + + def testToDirectedAcyclic(self): + graph = Graph([(0, 1), (2, 0), (3, 0), (3, 0), (4, 2)], directed=False) + graph.to_directed("acyclic") + self.assertTrue(graph.is_directed()) + self.assertTrue(graph.vcount() == 5) + print(graph.get_edgelist()) + self.assertTrue( + sorted(graph.get_edgelist()) + == [(0, 1), (0, 2), (0, 3), (0, 3), (2, 4)] + ) + + def testToDirectedRandom(self): + random.seed(0) + + graph = Graph.Ring(200, directed=False) + graph.to_directed("random") + + self.assertTrue(graph.is_directed()) + self.assertTrue(graph.vcount() == 200) + edgelist1 = sorted(graph.get_edgelist()) + + graph = Graph.Ring(200, directed=False) + graph.to_directed("random") + + self.assertTrue(graph.is_directed()) + self.assertTrue(graph.vcount() == 200) + edgelist2 = sorted(graph.get_edgelist()) + + self.assertTrue(edgelist1 != edgelist2) + + def testToDirectedInvalidMode(self): + graph = Graph([(0, 1), (0, 2), (2, 3), (2, 4)], directed=False) + with self.assertRaises(ValueError): + graph.to_directed("no-such-mode") + class GraphRepresentationTests(unittest.TestCase): def testGetAdjacency(self): From 3146714fff466c486d0dca57ffcc599e92919874 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 5 May 2021 18:19:14 +0200 Subject: [PATCH 0281/1681] feat: updated igraph to 0.9.3 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index ecbe39a4e..a4be06863 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit ecbe39a4eb8d4319633e3e364fd72fe7fe9ae109 +Subproject commit a4be0686333f2e9fe38b3bc7a23b9073e848f77a From 65b1e4c24edf9c60b3d83bfa9dc34b3697a3d381 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 5 May 2021 18:20:14 +0200 Subject: [PATCH 0282/1681] doc: updated changelog, slowly preparing for 0.9.3 --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d7a9f11c..caaffc782 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## Unreleased +## 0.9.3 ### Added @@ -13,10 +13,15 @@ * `Graph.to_directed()` now supports a `mode=...` keyword argument. +### Changed + +* Updated igraph dependency to 0.9.3. + ### Deprecated * `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. + ## 0.9.1 ### Changed From 10d9d2e010f0f18c5038e009d54a89e50922465d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 5 May 2021 18:32:22 +0200 Subject: [PATCH 0283/1681] fix: added missing closing bracket that I accidentally removed in the previous commit --- src/_igraph/graphobject.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index cacb41592..31ffe0caf 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14832,6 +14832,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " C{\"random\"} picks a random direction for each edge. C{\"acyclic\"}\n" " picks the edge directions in a way that the resulting graph will be\n" " acyclic if there were no self-loops in the original graph.\n" + }, // interface to igraph_to_undirected {"to_undirected", (PyCFunction) igraphmodule_Graph_to_undirected, From 52df5180289cfe916766dab4022fc6cc35caf86b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 5 May 2021 18:32:51 +0200 Subject: [PATCH 0284/1681] fix: deprecated partial string matches in conversions to enum members, closes #387 --- CHANGELOG.md | 6 ++++ src/_igraph/convert.c | 82 ++++++++++++++++++++++++++++--------------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index caaffc782..b9edadb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ ### Deprecated +* Functions and methods that take string arguments that represent an underlying + enum in the C core of igraph now print a deprecation warning when provided + with a string that does not match one of the enum member names (as documented + in the docstrings) exactly. Partial matches will be removed in the next + minor or major version, whichever comes first. + * `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index b9f0ad18d..269b7bccc 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -83,42 +83,68 @@ int PyLong_AsInt(PyObject* obj, int* result) { int igraphmodule_PyObject_to_enum(PyObject *o, igraphmodule_enum_translation_table_entry_t* table, int *result) { - char *s, *s2; - int i, best, best_result, best_unique; - if (o == 0 || o == Py_None) + char *s, *s2; + int i, best, best_result, best_unique; + + if (o == 0 || o == Py_None) + return 0; + + if (PyLong_Check(o)) + return PyLong_AsInt(o, result); + + s = PyUnicode_CopyAsString(o); + if (s == 0) { + PyErr_SetString(PyExc_TypeError, "int, long or string expected"); + return -1; + } + + /* Convert string to lowercase */ + for (s2 = s; *s2; s2++) { + *s2 = tolower(*s2); + } + + /* Search for matches */ + best = 0; best_unique = 0; best_result = -1; + while (table->name != 0) { + if (strcmp(s, table->name) == 0) { + /* Exact match found */ + *result = table->value; + free(s); return 0; - if (PyLong_Check(o)) - return PyLong_AsInt(o, result); - s = PyUnicode_CopyAsString(o); - if (s == 0) { - PyErr_SetString(PyExc_TypeError, "int, long or string expected"); - return -1; } - /* Convert string to lowercase */ - for (s2=s; *s2; s2++) - *s2 = tolower(*s2); - best = 0; best_unique = 0; best_result = -1; - /* Search for matches */ - while (table->name != 0) { - if (strcmp(s, table->name) == 0) { - *result = table->value; - free(s); - return 0; - } - for (i=0; s[i] == table->name[i]; i++); - if (i > best) { - best = i; best_unique = 1; best_result = table->value; - } else if (i == best) best_unique = 0; - table++; + + /* Find length of longest prefix that matches */ + for (i = 0; s[i] == table->name[i]; i++); + + if (i > best) { + /* Found a better match than before */ + best = i; best_unique = 1; best_result = table->value; + } else if (i == best) { + /* Best match is not unique */ + best_unique = 0; } - free(s); - if (best_unique) { *result = best_result; return 0; } + + table++; + } + + free(s); + + if (best_unique) { + PyErr_Warn( + PyExc_DeprecationWarning, + "Partial string matches of enum members are deprecated since igraph 0.9.3; " + "use strings that identify an enum member unambiguously." + ); + + *result = best_result; + return 0; + } else { PyErr_SetObject(PyExc_ValueError, o); return -1; + } } - /** * \ingroup python_interface_conversion * \brief Converts a Python object to a corresponding igraph enum, strictly. From 1c87aa9a0118d66fde21fcc6de6ffb2f13851b5b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 21 May 2021 11:17:43 +0200 Subject: [PATCH 0285/1681] doc: fix quoting of 'None' in igraph.drawing.plot() --- src/igraph/drawing/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index e099c0dd8..8c9723b40 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -465,7 +465,7 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): is 20 on each side. @keyword inline: whether to try and show the plot object inline in the - current IPython notebook. Passing ``None`` here or omitting this keyword + current IPython notebook. Passing C{None} here or omitting this keyword argument will look up the preferred behaviour from the C{shell.ipython.inlining.Plot} configuration key. Note that this keyword argument has an effect only if igraph is run inside IPython and C{target} From f6eb8cf93443d3cca8d52716979bba971c047911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Thu, 27 May 2021 20:24:04 +0200 Subject: [PATCH 0286/1681] improve from_networkx performance --- src/igraph/__init__.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 547c56a85..8995047b5 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1920,6 +1920,7 @@ def from_networkx(cls, g): @param g: networkx Graph or DiGraph """ import networkx as nx + from collections import defaultdict # Graph attributes gattr = dict(g.graph) @@ -1942,13 +1943,15 @@ def from_networkx(cls, g): graph.vs[vd[v]][key] = val # Edges and edge attributes - # NOTE: we need to do both together to deal well with multigraphs - # Each e might have a length of 2 (graphs) or 3 (multigraphs, the - # third element is the "color" of the edge) - for e, (_, _, datum) in zip(g.edges, g.edges.data()): - eid = graph.add_edge(vd[e[0]], vd[e[1]]) - for key, val in list(datum.items()): - eid[key] = val + eattr_names = {name for (_, _, data) in g.edges.data() for name in data} + eattr = defaultdict(list) + edges = [] + for (u, v, data) in g.edges.data(): + edges.append((vd[u], vd[v])) + for name in eattr_names: + eattr[name].append(data.get(name)) + + graph.add_edges(edges, eattr) return graph From 941f43aea795a09308d4de32b61792e32a403b3c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 27 May 2021 22:28:56 +0200 Subject: [PATCH 0287/1681] test: test cleanup, moved leftover from src/igraph/test to tests/ --- src/igraph/test/foreign.py | 579 ------------------------------------- tests/test_cliques.py | 2 +- tests/test_edgeseq.py | 6 +- tests/test_foreign.py | 241 ++++++++++++++- tests/test_operators.py | 4 +- tests/test_vertexseq.py | 6 +- tests/utils.py | 44 +-- 7 files changed, 242 insertions(+), 640 deletions(-) delete mode 100644 src/igraph/test/foreign.py diff --git a/src/igraph/test/foreign.py b/src/igraph/test/foreign.py deleted file mode 100644 index 49a4fa5d2..000000000 --- a/src/igraph/test/foreign.py +++ /dev/null @@ -1,579 +0,0 @@ -import sys -import io -import unittest -import warnings - -from igraph import Graph, InternalError -from igraph.test.utils import temporary_file - - -try: - import networkx as nx -except ImportError: - nx = None - - -try: - import graph_tool as gt -except ImportError: - gt = None - - -class ForeignTests(unittest.TestCase): - def testDIMACS(self): - with temporary_file( - """\ - c - c This is a simple example file to demonstrate the - c DIMACS input file format for minimum-cost flow problems. - c - c problem line : - p max 4 5 - c - c node descriptor lines : - n 1 s - n 4 t - c - c arc descriptor lines : - a 1 2 4 - a 1 3 2 - a 2 3 2 - a 2 4 3 - a 3 4 5 - """ - ) as tmpfname: - graph = Graph.Read_DIMACS(tmpfname, False) - self.assertTrue(isinstance(graph, Graph)) - self.assertTrue(graph.vcount() == 4 and graph.ecount() == 5) - self.assertTrue(graph["source"] == 0 and graph["target"] == 3) - self.assertTrue(graph.es["capacity"] == [4, 2, 2, 3, 5]) - graph.write_dimacs(tmpfname) - - def testDL(self): - with temporary_file( - """\ - dl n=5 - format = fullmatrix - labels embedded - data: - larry david lin pat russ - Larry 0 1 1 1 0 - david 1 0 0 0 1 - Lin 1 0 0 1 0 - Pat 1 0 1 0 1 - russ 0 1 0 1 0 - """ - ) as tmpfname: - g = Graph.Read_DL(tmpfname) - self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 5 and g.ecount() == 12) - self.assertTrue(g.is_directed()) - self.assertTrue( - sorted(g.get_edgelist()) - == [ - (0, 1), - (0, 2), - (0, 3), - (1, 0), - (1, 4), - (2, 0), - (2, 3), - (3, 0), - (3, 2), - (3, 4), - (4, 1), - (4, 3), - ] - ) - - with temporary_file( - """\ - dl n=5 - format = fullmatrix - labels: - barry,david - lin,pat - russ - data: - 0 1 1 1 0 - 1 0 0 0 1 - 1 0 0 1 0 - 1 0 1 0 1 - 0 1 0 1 0 - """ - ) as tmpfname: - g = Graph.Read_DL(tmpfname) - self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 5 and g.ecount() == 12) - self.assertTrue(g.is_directed()) - self.assertTrue( - sorted(g.get_edgelist()) - == [ - (0, 1), - (0, 2), - (0, 3), - (1, 0), - (1, 4), - (2, 0), - (2, 3), - (3, 0), - (3, 2), - (3, 4), - (4, 1), - (4, 3), - ] - ) - - with temporary_file( - """\ - DL n=5 - format = edgelist1 - labels: - george, sally, jim, billy, jane - labels embedded: - data: - george sally 2 - george jim 3 - sally jim 4 - billy george 5 - jane jim 6 - """ - ) as tmpfname: - g = Graph.Read_DL(tmpfname, False) - self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 5 and g.ecount() == 5) - self.assertTrue(not g.is_directed()) - self.assertTrue( - sorted(g.get_edgelist()) == [(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)] - ) - - def _testNCOLOrLGL(self, func, fname, can_be_reopened=True): - g = func(fname, names=False, weights=False, directed=False) - self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 4 and g.ecount() == 5) - self.assertTrue(not g.is_directed()) - self.assertTrue( - sorted(g.get_edgelist()) == [(0, 1), (0, 2), (1, 1), (1, 3), (2, 3)] - ) - self.assertTrue( - "name" not in g.vertex_attributes() and "weight" not in g.edge_attributes() - ) - if not can_be_reopened: - return - - g = func(fname, names=False, directed=False) - self.assertTrue( - "name" not in g.vertex_attributes() and "weight" in g.edge_attributes() - ) - self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) - - g = func(fname, directed=False) - self.assertTrue( - "name" in g.vertex_attributes() and "weight" in g.edge_attributes() - ) - self.assertTrue(g.vs["name"] == ["eggs", "spam", "ham", "bacon"]) - self.assertTrue(g.es["weight"] == [1, 2, 0, 3, 0]) - - def testNCOL(self): - with temporary_file( - """\ - eggs spam 1 - ham eggs 2 - ham bacon - bacon spam 3 - spam spam""" - ) as tmpfname: - self._testNCOLOrLGL(func=Graph.Read_Ncol, fname=tmpfname) - - with temporary_file( - """\ - eggs spam - ham eggs - ham bacon - bacon spam - spam spam""" - ) as tmpfname: - g = Graph.Read_Ncol(tmpfname) - self.assertTrue( - "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() - ) - - def testLGL(self): - with temporary_file( - """\ - # eggs - spam 1 - # ham - eggs 2 - bacon - # bacon - spam 3 - # spam - spam""" - ) as tmpfname: - self._testNCOLOrLGL(func=Graph.Read_Lgl, fname=tmpfname) - - with temporary_file( - """\ - # eggs - spam - # ham - eggs - bacon - # bacon - spam - # spam - spam""" - ) as tmpfname: - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - g = Graph.Read_Lgl(tmpfname) - self.assertTrue( - "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() - ) - - # This is not an LGL file; we are testing error handling here - with temporary_file( - """\ - 1 2 - 1 3 - """ - ) as tmpfname: - with self.assertRaises(InternalError): - Graph.Read_Lgl(tmpfname) - - def testLGLWithIOModule(self): - with temporary_file( - """\ - # eggs - spam 1 - # ham - eggs 2 - bacon - # bacon - spam 3 - # spam - spam""" - ) as tmpfname: - with io.open(tmpfname, "r") as fp: - self._testNCOLOrLGL( - func=Graph.Read_Lgl, fname=fp, can_be_reopened=False - ) - - def testAdjacency(self): - with temporary_file( - """\ - # Test comment line - 0 1 1 0 0 0 - 1 0 1 0 0 0 - 1 1 0 0 0 0 - 0 0 0 0 2 2 - 0 0 0 2 0 2 - 0 0 0 2 2 0 - """ - ) as tmpfname: - g = Graph.Read_Adjacency(tmpfname) - self.assertTrue(isinstance(g, Graph)) - self.assertTrue( - g.vcount() == 6 - and g.ecount() == 18 - and g.is_directed() - and "weight" not in g.edge_attributes() - ) - g = Graph.Read_Adjacency(tmpfname, attribute="weight") - self.assertTrue(isinstance(g, Graph)) - self.assertTrue( - g.vcount() == 6 - and g.ecount() == 12 - and g.is_directed() - and g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2] - ) - - g.write_adjacency(tmpfname) - - def testPickle(self): - pickle = [ - 128, - 2, - 99, - 105, - 103, - 114, - 97, - 112, - 104, - 10, - 71, - 114, - 97, - 112, - 104, - 10, - 113, - 1, - 40, - 75, - 3, - 93, - 113, - 2, - 75, - 1, - 75, - 2, - 134, - 113, - 3, - 97, - 137, - 125, - 125, - 125, - 116, - 82, - 113, - 4, - 125, - 98, - 46, - ] - if sys.version_info > (3, 0): - pickle = bytes(pickle) - else: - pickle = "".join(map(chr, pickle)) - with temporary_file(pickle, "wb") as tmpfname: - g = Graph.Read_Pickle(pickle) - self.assertTrue(isinstance(g, Graph)) - self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) - g.write_pickle(tmpfname) - - @unittest.skipIf(nx is None, "test case depends on networkx") - def testGraphNetworkx(self): - # Undirected - g = Graph.Ring(10) - g["gattr"] = "graph_attribute" - g.vs["vattr"] = list(range(g.vcount())) - g.es["eattr"] = list(range(len(g.es))) - - # Go to networkx and back - g_nx = g.to_networkx() - g2 = Graph.from_networkx(g_nx) - - self.assertFalse(g2.is_directed()) - self.assertTrue(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - # Test attributes - self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) - for i, vertex in enumerate(g.vs): - vertex2 = g2.vs[i] - for an in vertex.attribute_names(): - if an == "vattr": - continue - self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) - self.assertEqual(g.edge_attributes(), g2.edge_attributes()) - for edge in g.es: - eid = g2.get_eid(edge.source, edge.target) - edge2 = g2.es[eid] - for an in edge.attribute_names(): - self.assertEqual(edge.attributes()[an], edge2.attributes()[an]) - - # Directed - g = Graph.Ring(10, directed=True) - - # Go to networkx and back - g_nx = g.to_networkx() - g2 = Graph.from_networkx(g_nx) - - self.assertTrue(g2.is_directed()) - self.assertTrue(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - @unittest.skipIf(nx is None, "test case depends on networkx") - def testMultigraphNetworkx(self): - # Undirected - g = Graph.Ring(10) - g.add_edge(0, 1) - g["gattr"] = "graph_attribute" - g.vs["vattr"] = list(range(g.vcount())) - g.es["eattr"] = list(range(len(g.es))) - - # Go to networkx and back - g_nx = g.to_networkx() - g2 = Graph.from_networkx(g_nx) - - self.assertFalse(g2.is_directed()) - self.assertFalse(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - # Test attributes - self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) - self.assertEqual(g.edge_attributes(), g2.edge_attributes()) - # Testing parallel edges is a bit more tricky - edge2_found = set() - for edge in g.es: - # Go through all parallel edges between these two vertices - for edge2 in g2.es: - if edge2 in edge2_found: - continue - if edge.source != edge2.source: - continue - if edge.target != edge2.target: - continue - # Check all attributes between these two - for an in edge.attribute_names(): - if edge.attributes()[an] != edge2.attributes()[an]: - break - else: - # Correspondence found - edge2_found.add(edge2) - break - - else: - self.assertTrue(False) - - # Directed - g = Graph.Ring(10, directed=True) - g.add_edge(0, 1) - - # Go to networkx and back - g_nx = g.to_networkx() - g2 = Graph.from_networkx(g_nx) - - self.assertTrue(g2.is_directed()) - self.assertFalse(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - @unittest.skipIf(gt is None, "test case depends on graph-tool") - def testGraphGraphTool(self): - # Undirected - g = Graph.Ring(10) - g["gattr"] = "graph_attribute" - g.vs["vattr"] = list(range(g.vcount())) - g.es["eattr"] = list(range(len(g.es))) - - # Go to graph-tool and back - g_gt = g.to_graph_tool( - graph_attributes={"gattr": "object"}, - vertex_attributes={"vattr": "int"}, - edge_attributes={"eattr": "int"}, - ) - g2 = Graph.from_graph_tool(g_gt) - - self.assertFalse(g2.is_directed()) - self.assertTrue(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - # Test attributes - self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual(g.vertex_attributes(), g2.vertex_attributes()) - for i, vertex in enumerate(g.vs): - vertex2 = g2.vs[i] - for an in vertex.attribute_names(): - self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) - self.assertEqual(g.edge_attributes(), g2.edge_attributes()) - for edge in g.es: - eid = g2.get_eid(edge.source, edge.target) - edge2 = g2.es[eid] - for an in edge.attribute_names(): - self.assertEqual(edge.attributes()[an], edge2.attributes()[an]) - - # Directed - g = Graph.Ring(10, directed=True) - - # Go to graph-tool and back - g_gt = g.to_graph_tool() - - g2 = Graph.from_graph_tool(g_gt) - - self.assertTrue(g2.is_directed()) - self.assertTrue(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - @unittest.skipIf(gt is None, "test case depends on graph-tool") - def testMultigraphGraphTool(self): - # Undirected - g = Graph.Ring(10) - g.add_edge(0, 1) - g["gattr"] = "graph_attribute" - g.vs["vattr"] = list(range(g.vcount())) - g.es["eattr"] = list(range(len(g.es))) - - # Go to graph-tool and back - g_gt = g.to_graph_tool( - graph_attributes={"gattr": "object"}, - vertex_attributes={"vattr": "int"}, - edge_attributes={"eattr": "int"}, - ) - g2 = Graph.from_graph_tool(g_gt) - - self.assertFalse(g2.is_directed()) - self.assertFalse(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - # Test attributes - self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual(g.vertex_attributes(), g2.vertex_attributes()) - for i, vertex in enumerate(g.vs): - vertex2 = g2.vs[i] - for an in vertex.attribute_names(): - self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) - self.assertEqual(g.edge_attributes(), g2.edge_attributes()) - # Testing parallel edges is a bit more tricky - edge2_found = set() - for edge in g.es: - # Go through all parallel edges between these two vertices - for edge2 in g2.es: - if edge2 in edge2_found: - continue - if edge.source != edge2.source: - continue - if edge.target != edge2.target: - continue - # Check all attributes between these two - for an in edge.attribute_names(): - if edge.attributes()[an] != edge2.attributes()[an]: - break - else: - # Correspondence found - edge2_found.add(edge2) - break - - else: - self.assertTrue(False) - - # Directed - g = Graph.Ring(10, directed=True) - g.add_edge(0, 1) - - # Go to graph-tool and back - g_gt = g.to_graph_tool() - g2 = Graph.from_graph_tool(g_gt) - - self.assertTrue(g2.is_directed()) - self.assertFalse(g2.is_simple()) - self.assertEqual(g.vcount(), g2.vcount()) - self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) - - -def suite(): - foreign_suite = unittest.makeSuite(ForeignTests) - return unittest.TestSuite([foreign_suite]) - - -def test(): - runner = unittest.TextTestRunner() - runner.run(suite()) - - -if __name__ == "__main__": - test() diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 70a958376..0ef0c69ab 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -2,7 +2,7 @@ from igraph import * -from .utils import is_pypy, skipIf, temporary_file +from .utils import temporary_file class CliqueTests(unittest.TestCase): diff --git a/tests/test_edgeseq.py b/tests/test_edgeseq.py index 6ec581d70..74667b758 100644 --- a/tests/test_edgeseq.py +++ b/tests/test_edgeseq.py @@ -4,7 +4,7 @@ from igraph import * -from .utils import is_pypy, skipIf +from .utils import is_pypy try: import numpy as np @@ -85,7 +85,7 @@ def testPhantomEdge(self): self.assertRaises(ValueError, getattr, e, "tuple") self.assertRaises(ValueError, getattr, e, "vertex_tuple") - @skipIf(is_pypy, "skipped on PyPy because we do not have access to docstrings") + @unittest.skipIf(is_pypy, "skipped on PyPy because we do not have access to docstrings") def testProxyMethods(self): g = Graph.GRG(10, 0.5) e = g.es[0] @@ -148,7 +148,7 @@ def testIndexing(self): self.assertRaises(IndexError, self.g.es.__getitem__, -n - 1) self.assertRaises(TypeError, self.g.es.__getitem__, 1.5) - @skipIf(np is None, "test case depends on NumPy") + @unittest.skipIf(np is None, "test case depends on NumPy") def testNumPyIndexing(self): n = self.g.ecount() for i in range(n): diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 630a612fc..a3db0b869 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -2,9 +2,19 @@ import unittest import warnings -from igraph import * +from igraph import Graph, InternalError -from .utils import temporary_file, skipIf +from .utils import temporary_file + +try: + import networkx as nx +except ImportError: + nx = None + +try: + import graph_tool as gt +except ImportError: + gt = None try: import pandas as pd @@ -390,17 +400,14 @@ def testPickle(self): 98, 46, ] - if sys.version_info > (3, 0): - pickle = bytes(pickle) - else: - pickle = "".join(map(chr, pickle)) + pickle = bytes(pickle) with temporary_file(pickle, "wb", binary=True) as tmpfname: g = Graph.Read_Pickle(pickle) self.assertTrue(isinstance(g, Graph)) self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) - @skipIf(pd is None, "test case depends on Pandas") + @unittest.skipIf(pd is None, "test case depends on Pandas") def testVertexDataFrames(self): g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) @@ -435,7 +442,7 @@ def testVertexDataFrames(self): self.assertEqual(list(df.columns), ["weight"]) self.assertEqual(list(df["weight"]), g.vs["weight"]) - @skipIf(pd is None, "test case depends on Pandas") + @unittest.skipIf(pd is None, "test case depends on Pandas") def testEdgeDataFrames(self): g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) @@ -479,6 +486,224 @@ def testEdgeDataFrames(self): self.assertEqual(list(df.iloc[:, i]), g.es["source"]) + @unittest.skipIf(nx is None, "test case depends on networkx") + def testGraphNetworkx(self): + # Undirected + g = Graph.Ring(10) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertFalse(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) + for i, vertex in enumerate(g.vs): + vertex2 = g2.vs[i] + for an in vertex.attribute_names(): + if an == "vattr": + continue + self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + for edge in g.es: + eid = g2.get_eid(edge.source, edge.target) + edge2 = g2.es[eid] + for an in edge.attribute_names(): + self.assertEqual(edge.attributes()[an], edge2.attributes()[an]) + + # Directed + g = Graph.Ring(10, directed=True) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertTrue(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + @unittest.skipIf(nx is None, "test case depends on networkx") + def testMultigraphNetworkx(self): + # Undirected + g = Graph.Ring(10) + g.add_edge(0, 1) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertFalse(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + # Testing parallel edges is a bit more tricky + edge2_found = set() + for edge in g.es: + # Go through all parallel edges between these two vertices + for edge2 in g2.es: + if edge2 in edge2_found: + continue + if edge.source != edge2.source: + continue + if edge.target != edge2.target: + continue + # Check all attributes between these two + for an in edge.attribute_names(): + if edge.attributes()[an] != edge2.attributes()[an]: + break + else: + # Correspondence found + edge2_found.add(edge2) + break + + else: + self.assertTrue(False) + + # Directed + g = Graph.Ring(10, directed=True) + g.add_edge(0, 1) + + # Go to networkx and back + g_nx = g.to_networkx() + g2 = Graph.from_networkx(g_nx) + + self.assertTrue(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + @unittest.skipIf(gt is None, "test case depends on graph-tool") + def testGraphGraphTool(self): + # Undirected + g = Graph.Ring(10) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) + + # Go to graph-tool and back + g_gt = g.to_graph_tool( + graph_attributes={"gattr": "object"}, + vertex_attributes={"vattr": "int"}, + edge_attributes={"eattr": "int"}, + ) + g2 = Graph.from_graph_tool(g_gt) + + self.assertFalse(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual(g.vertex_attributes(), g2.vertex_attributes()) + for i, vertex in enumerate(g.vs): + vertex2 = g2.vs[i] + for an in vertex.attribute_names(): + self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + for edge in g.es: + eid = g2.get_eid(edge.source, edge.target) + edge2 = g2.es[eid] + for an in edge.attribute_names(): + self.assertEqual(edge.attributes()[an], edge2.attributes()[an]) + + # Directed + g = Graph.Ring(10, directed=True) + + # Go to graph-tool and back + g_gt = g.to_graph_tool() + + g2 = Graph.from_graph_tool(g_gt) + + self.assertTrue(g2.is_directed()) + self.assertTrue(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + @unittest.skipIf(gt is None, "test case depends on graph-tool") + def testMultigraphGraphTool(self): + # Undirected + g = Graph.Ring(10) + g.add_edge(0, 1) + g["gattr"] = "graph_attribute" + g.vs["vattr"] = list(range(g.vcount())) + g.es["eattr"] = list(range(len(g.es))) + + # Go to graph-tool and back + g_gt = g.to_graph_tool( + graph_attributes={"gattr": "object"}, + vertex_attributes={"vattr": "int"}, + edge_attributes={"eattr": "int"}, + ) + g2 = Graph.from_graph_tool(g_gt) + + self.assertFalse(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + # Test attributes + self.assertEqual(g.attributes(), g2.attributes()) + self.assertEqual(g.vertex_attributes(), g2.vertex_attributes()) + for i, vertex in enumerate(g.vs): + vertex2 = g2.vs[i] + for an in vertex.attribute_names(): + self.assertEqual(vertex.attributes()[an], vertex2.attributes()[an]) + self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + # Testing parallel edges is a bit more tricky + edge2_found = set() + for edge in g.es: + # Go through all parallel edges between these two vertices + for edge2 in g2.es: + if edge2 in edge2_found: + continue + if edge.source != edge2.source: + continue + if edge.target != edge2.target: + continue + # Check all attributes between these two + for an in edge.attribute_names(): + if edge.attributes()[an] != edge2.attributes()[an]: + break + else: + # Correspondence found + edge2_found.add(edge2) + break + + else: + self.assertTrue(False) + + # Directed + g = Graph.Ring(10, directed=True) + g.add_edge(0, 1) + + # Go to graph-tool and back + g_gt = g.to_graph_tool() + g2 = Graph.from_graph_tool(g_gt) + + self.assertTrue(g2.is_directed()) + self.assertFalse(g2.is_simple()) + self.assertEqual(g.vcount(), g2.vcount()) + self.assertEqual(sorted(g.get_edgelist()), sorted(g2.get_edgelist())) + + def suite(): foreign_suite = unittest.makeSuite(ForeignTests) return unittest.TestSuite([foreign_suite]) diff --git a/tests/test_operators.py b/tests/test_operators.py index ec001dd46..04a0fe08e 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -2,8 +2,6 @@ from igraph import * -from .utils import skipIf - try: import numpy as np except ImportError: @@ -476,7 +474,7 @@ def testContractVertices(self): self.assertEqual(g2.vcount(), 5) self.assertEqual(g2.ecount(), 0) - @skipIf(np is None, "test case depends on NumPy") + @unittest.skipIf(np is None, "test case depends on NumPy") def testContractVerticesWithNumPyIntegers(self): g = Graph.Full(4) + Graph.Full(4) + [(0, 5), (1, 4)] g2 = g.copy() diff --git a/tests/test_vertexseq.py b/tests/test_vertexseq.py index 39037e4a1..12adf1aa1 100644 --- a/tests/test_vertexseq.py +++ b/tests/test_vertexseq.py @@ -4,7 +4,7 @@ from igraph import * -from .utils import is_pypy, skipIf +from .utils import is_pypy try: import numpy as np @@ -99,7 +99,7 @@ def testNeighbors(self): [edge.index for edge in vertex.neighbors(mode=mode)], ) - @skipIf(is_pypy, "skipped on PyPy because we do not have access to docstrings") + @unittest.skipIf(is_pypy, "skipped on PyPy because we do not have access to docstrings") def testProxyMethods(self): # We only test with connected graphs because disconnected graphs might # print a warning when shortest_paths() is invoked on them and we want @@ -177,7 +177,7 @@ def testIndexing(self): self.assertRaises(IndexError, self.g.vs.__getitem__, -n - 1) self.assertRaises(TypeError, self.g.vs.__getitem__, 1.5) - @skipIf(np is None, "test case depends on NumPy") + @unittest.skipIf(np is None, "test case depends on NumPy") def testNumPyIndexing(self): n = self.g.vcount() for i in range(self.g.vcount()): diff --git a/tests/utils.py b/tests/utils.py index 8bfbc88d4..fdea66dba 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,55 +1,13 @@ """Utility functions for unit testing.""" -import functools import os import platform -import sys import tempfile -import types from contextlib import contextmanager from textwrap import dedent -__all__ = ["skip", "skipIf", "temporary_file"] - - -def _id(obj): - return obj - - -try: - from unittest import skip -except ImportError: - # Provide basic replacement for unittest.skip - def skip(reason): - """Unconditionally skip a test.""" - - def decorator(test_item): - if not isinstance(test_item, type): - - @functools.wraps(test_item) - def skip_wrapper(*args, **kwds): - if reason: - sys.stderr.write("skipped, %s ... " % reason) - else: - sys.stderr.write("skipped, ") - return - - test_item = skip_wrapper - return test_item - - return decorator - - -try: - from unittest import skipIf -except ImportError: - # Provide basic replacement for unittest.skipIf - def skipIf(condition, reason): - """Skip a test if the condition is true.""" - if condition: - return skip(reason) - return _id +__all__ = ("temporary_file", ) @contextmanager From 8d356a97a6ed6bee3f34a11592a86263f0053ccf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 27 May 2021 22:39:44 +0200 Subject: [PATCH 0288/1681] test: fix deprecation warnings in unit tests --- src/_igraph/convert.c | 3 +++ src/igraph/clustering.py | 2 +- tests/test_conversion.py | 1 - tests/test_decomposition.py | 2 +- tests/test_structural.py | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 269b7bccc..e54548bff 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -266,6 +266,7 @@ int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, static igraphmodule_enum_translation_table_entry_t attribute_combination_type_tt[] = { {"ignore", IGRAPH_ATTRIBUTE_COMBINE_IGNORE}, {"sum", IGRAPH_ATTRIBUTE_COMBINE_SUM}, + {"prod", IGRAPH_ATTRIBUTE_COMBINE_PROD}, {"product", IGRAPH_ATTRIBUTE_COMBINE_PROD}, {"min", IGRAPH_ATTRIBUTE_COMBINE_MIN}, {"max", IGRAPH_ATTRIBUTE_COMBINE_MAX}, @@ -274,6 +275,7 @@ int igraphmodule_PyObject_to_attribute_combination_type_t(PyObject* o, {"last", IGRAPH_ATTRIBUTE_COMBINE_LAST}, {"mean", IGRAPH_ATTRIBUTE_COMBINE_MEAN}, {"median", IGRAPH_ATTRIBUTE_COMBINE_MEDIAN}, + {"concat", IGRAPH_ATTRIBUTE_COMBINE_CONCAT}, {"concatenate", IGRAPH_ATTRIBUTE_COMBINE_CONCAT}, {0, 0} }; @@ -483,6 +485,7 @@ int igraphmodule_PyObject_to_community_comparison_t(PyObject *o, {"nmi", IGRAPH_COMMCMP_NMI}, {"danon", IGRAPH_COMMCMP_NMI}, {"split-join", IGRAPH_COMMCMP_SPLIT_JOIN}, + {"split_join", IGRAPH_COMMCMP_SPLIT_JOIN}, {"rand", IGRAPH_COMMCMP_RAND}, {"adjusted_rand", IGRAPH_COMMCMP_ADJUSTED_RAND}, {0,0} diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index e54dc35ac..3c00b3c95 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -352,7 +352,7 @@ def cluster_graph(self, combine_vertices=None, combine_edges=None): """ result = self.graph.copy() result.contract_vertices(self.membership, combine_vertices) - if combine_edges != False: + if combine_edges is not False: result.simplify(combine_edges=combine_edges) return result diff --git a/tests/test_conversion.py b/tests/test_conversion.py index aac08af5a..b8600e9a3 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -64,7 +64,6 @@ def testToDirectedAcyclic(self): graph.to_directed("acyclic") self.assertTrue(graph.is_directed()) self.assertTrue(graph.vcount() == 5) - print(graph.get_edgelist()) self.assertTrue( sorted(graph.get_edgelist()) == [(0, 1), (0, 2), (0, 3), (0, 3), (2, 4)] diff --git a/tests/test_decomposition.py b/tests/test_decomposition.py index 8413af0d6..21e25e6f8 100644 --- a/tests/test_decomposition.py +++ b/tests/test_decomposition.py @@ -583,7 +583,7 @@ def testCompareNMI(self): def testCompareSplitJoin(self): expected = [0, 3, 5, 11] - self._testMethod("split", expected) + self._testMethod("split_join", expected) l1 = [1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3] l2 = [3, 1, 2, 1, 3, 1, 3, 1, 2, 1, 4, 2] self.assertEqual(split_join_distance(l1, l2), (6, 5)) diff --git a/tests/test_structural.py b/tests/test_structural.py index 545733d1e..9ea13d990 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -434,7 +434,7 @@ def testHubScore(self): def testCoreness(self): g = Graph.Full(4) + Graph(4) + [(0, 4), (1, 5), (2, 6), (3, 7)] - self.assertEqual(g.coreness("A"), [3, 3, 3, 3, 1, 1, 1, 1]) + self.assertEqual(g.coreness("all"), [3, 3, 3, 3, 1, 1, 1, 1]) class NeighborhoodTests(unittest.TestCase): From 5e937667ef7e7ac431b23b948483a98df5066655 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 28 May 2021 11:47:39 +0200 Subject: [PATCH 0289/1681] fix: keep on supporting 'min' and 'max' as aliases to 'minimum' and 'maximum' in igraph_adjacency_t conversion --- src/_igraph/convert.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index e54548bff..f45cc4d3e 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -254,6 +254,8 @@ int igraphmodule_PyObject_to_adjacency_t(PyObject *o, {"lower", IGRAPH_ADJ_LOWER}, {"minimum", IGRAPH_ADJ_MIN}, {"maximum", IGRAPH_ADJ_MAX}, + {"min", IGRAPH_ADJ_MIN}, + {"max", IGRAPH_ADJ_MAX}, {"plus", IGRAPH_ADJ_PLUS}, {0,0} }; From 1650bc6a33625ae6f503551b81039eb05a59c96b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 28 May 2021 11:49:04 +0200 Subject: [PATCH 0290/1681] style: fixing some linter warnings in __igraph__.py --- src/igraph/__init__.py | 229 ++++++++++++++++++++--------------------- 1 file changed, 109 insertions(+), 120 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 8995047b5..7a55a9eb9 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -65,7 +65,7 @@ Vertex, VertexSeq as _VertexSeq, WEAK, - arpack_options, + arpack_options as default_arpack_options, community_to_membership, convex_hull, is_degree_sequence, @@ -88,7 +88,7 @@ split_join_distance, ) from igraph.cut import Cut, Flow -from igraph.configuration import Configuration +from igraph.configuration import Configuration, init as init_configuration from igraph.drawing import BoundingBox, DefaultGraphDrawer, Plot, Point, Rectangle, plot from igraph.drawing.colors import ( Palette, @@ -730,9 +730,8 @@ def get_adjacency_sparse(self, attribute=None): from scipy import sparse except ImportError: raise ImportError( - "You should install scipy package in order to use this function" + "You should install scipy in order to use this function" ) - import numpy as np edges = self.get_edgelist() if attribute is None: @@ -1012,7 +1011,7 @@ def pagerank( @return: a list with the Google PageRank values of the specified vertices.""" if arpack_options is None: - arpack_options = _igraph.arpack_options + arpack_options = default_arpack_options return self.personalized_pagerank( vertices, directed, @@ -1304,7 +1303,8 @@ def community_label_propagation(self, weights=None, initial=None, fixed=None): @param fixed: a list of booleans for each vertex. C{True} corresponds to vertices whose labeling should not change during the algorithm. It only makes sense if initial labels are also given. Unlabeled - vertices cannot be fixed. + vertices cannot be fixed. It may also be the name of a vertex + attribute; each attribute value will be interpreted as a Boolean. @return: an appropriate L{VertexClustering} object. @newfield ref: Reference @@ -1314,7 +1314,7 @@ def community_label_propagation(self, weights=None, initial=None, fixed=None): U{http://arxiv.org/abs/0709.2938}. """ if isinstance(fixed, str): - fixed = [bool(o) for o in g.vs[fixed]] + fixed = [bool(o) for o in self.vs[fixed]] cl = GraphBase.community_label_propagation(self, weights, initial, fixed) return VertexClustering(self, cl, modularity_params=dict(weights=weights)) @@ -1526,7 +1526,7 @@ def k_core(self, *args): for arg in args: try: indices.extend(arg) - except: + except Exception: indices.append(arg) if len(indices) > 1 or hasattr(args[0], "__iter__"): @@ -1691,10 +1691,10 @@ def layout(self, layout=None, *args, **kwds): method = getattr(self.__class__, self._layout_mapping[layout]) if not hasattr(method, "__call__"): raise ValueError("layout method must be callable") - l = method(self, *args, **kwds) - if not isinstance(l, Layout): - l = Layout(l) - return l + layout = method(self, *args, **kwds) + if not isinstance(layout, Layout): + layout = Layout(layout) + return layout def layout_auto(self, *args, **kwds): """Chooses and runs a suitable layout function based on simple @@ -1763,19 +1763,6 @@ def layout_auto(self, *args, **kwds): algo = "drl" return self.layout(algo, *args, **kwds) - def layout_grid_fruchterman_reingold(self, *args, **kwds): - """Compatibility alias to the Fruchterman-Reingold layout with the grid - option turned on. - - @see: Graph.layout_fruchterman_reingold() - """ - deprecated( - "Graph.layout_grid_fruchterman_reingold() is deprecated since " - "igraph 0.8, please use Graph.layout_fruchterman_reingold(grid=True) instead" - ) - kwds["grid"] = True - return self.layout_fruchterman_reingold(*args, **kwds) - def layout_sugiyama( self, layers=None, @@ -2093,7 +2080,8 @@ def Read_Adjacency( @return: the created graph""" if isinstance(f, str): f = open(f) - matrix, ri, weights = [], 0, {} + + matrix, ri = [], 0 for line in f: line = line.strip() if len(line) == 0: @@ -2115,7 +2103,7 @@ def Read_Adjacency( return graph @classmethod - def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): + def Adjacency(cls, matrix, mode="directed", *args, **kwargs): """Generates a graph from its adjacency matrix. @param matrix: the adjacency matrix. Possible types are: @@ -2124,21 +2112,17 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): - a scipy.sparse matrix (will be converted to a COO matrix, but not to a dense matrix) @param mode: the mode to be used. Possible values are: - - - C{ADJ_DIRECTED} - the graph will be directed and a matrix + - C{"directed"} - the graph will be directed and a matrix element gives the number of edges between two vertex. - - C{ADJ_UNDIRECTED} - alias to C{ADJ_MAX} for convenience. - - C{ADJ_MAX} - undirected graph will be created and the number of + - C{"undirected"} - alias to C{"max"} for convenience. + - C{"max"} - undirected graph will be created and the number of edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - - C{ADJ_MIN} - like C{ADJ_MAX}, but with M{min(A(i,j), A(j,i))} - - C{ADJ_PLUS} - like C{ADJ_MAX}, but with M{A(i,j) + A(j,i)} - - C{ADJ_UPPER} - undirected graph with the upper right triangle of + - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} + - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} + - C{"upper"} - undirected graph with the upper right triangle of the matrix (including the diagonal) - - C{ADJ_LOWER} - undirected graph with the lower left triangle of + - C{"lower"} - undirected graph with the lower left triangle of the matrix (including the diagonal) - - These values can also be given as strings without the C{ADJ} prefix. - """ try: import numpy as np @@ -2151,7 +2135,7 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): sparse = None if (sparse is not None) and isinstance(matrix, sparse.spmatrix): - return _graph_from_sparse_matrix(klass, matrix, mode=mode) + return _graph_from_sparse_matrix(cls, matrix, mode=mode) if (np is not None) and isinstance(matrix, np.ndarray): matrix = matrix.tolist() @@ -2159,7 +2143,7 @@ def Adjacency(klass, matrix, mode=ADJ_DIRECTED, *args, **kwargs): return super().Adjacency(matrix, mode=mode) @classmethod - def Weighted_Adjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=True): + def Weighted_Adjacency(cls, matrix, mode="directed", attr="weight", loops=True): """Generates a graph from its weighted adjacency matrix. @param matrix: the adjacency matrix. Possible types are: @@ -2168,17 +2152,16 @@ def Weighted_Adjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=Tr - a scipy.sparse matrix (will be converted to a COO matrix, but not to a dense matrix) @param mode: the mode to be used. Possible values are: - - - C{ADJ_DIRECTED} - the graph will be directed and a matrix + - C{"directed"} - the graph will be directed and a matrix element gives the number of edges between two vertex. - - C{ADJ_UNDIRECTED} - alias to C{ADJ_MAX} for convenience. - - C{ADJ_MAX} - undirected graph will be created and the number of + - C{"undirected"} - alias to C{"max"} for convenience. + - C{"max"} - undirected graph will be created and the number of edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - - C{ADJ_MIN} - like C{ADJ_MAX}, but with M{min(A(i,j), A(j,i))} - - C{ADJ_PLUS} - like C{ADJ_MAX}, but with M{A(i,j) + A(j,i)} - - C{ADJ_UPPER} - undirected graph with the upper right triangle of + - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} + - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} + - C{"upper"} - undirected graph with the upper right triangle of the matrix (including the diagonal) - - C{ADJ_LOWER} - undirected graph with the lower left triangle of + - C{"lower"} - undirected graph with the lower left triangle of the matrix (including the diagonal) These values can also be given as strings without the C{ADJ} prefix. @@ -2200,7 +2183,7 @@ def Weighted_Adjacency(klass, matrix, mode=ADJ_DIRECTED, attr="weight", loops=Tr if sparse is not None and isinstance(matrix, sparse.spmatrix): return _graph_from_weighted_sparse_matrix( - klass, + cls, matrix, mode=mode, attr=attr, @@ -2269,8 +2252,6 @@ def write_graphmlz(self, f, compresslevel=9): @param compresslevel: the level of compression. 1 is fastest and produces the least compression, and 9 is slowest and produces the most compression.""" - from igraph.utils import named_temporary_file - with named_temporary_file() as tmpfile: self.write_graphml(tmpfile) outf = gzip.GzipFile(f, "wb", compresslevel) @@ -2318,8 +2299,6 @@ def Read_GraphMLz(cls, f, directed=True, index=0): start from zero, so if you want to load the first graph, specify 0 here. @return: the loaded graph object""" - from igraph.utils import named_temporary_file - with named_temporary_file() as tmpfile: with open(tmpfile, "wb") as outf: copyfileobj(gzip.GzipFile(f, "rb"), outf) @@ -2365,17 +2344,20 @@ def write_picklez(self, fname=None, version=-1): """ import pickle as pickle + file_was_opened = False + if not hasattr(fname, "write"): file_was_opened = True fname = gzip.open(fname, "wb") elif not isinstance(fname, gzip.GzipFile): file_was_opened = True fname = gzip.GzipFile(mode="wb", fileobj=fname) - else: - file_Was_opened = False + result = pickle.dump(self, fname, version) + if file_was_opened: fname.close() + return result @classmethod @@ -2392,7 +2374,6 @@ def Read_Pickle(cls, fname=None): # Probably a file or a file-like object result = pickle.load(fname) else: - fp = None try: fp = open(fname, "rb") except UnicodeDecodeError: @@ -2403,7 +2384,8 @@ def Read_Pickle(cls, fname=None): result = pickle.loads(fname) except TypeError: raise IOError( - "Cannot load file. If fname is a file name, that filename may be incorrect." + "Cannot load file. If fname is a file name, that " + "filename may be incorrect." ) except IOError: try: @@ -2411,35 +2393,20 @@ def Read_Pickle(cls, fname=None): result = pickle.loads(fname) except TypeError: raise IOError( - "Cannot load file. If fname is a file name, that filename may be incorrect." + "Cannot load file. If fname is a file name, that " + "filename may be incorrect." ) - if fp is not None: + else: result = pickle.load(fp) fp.close() - return result - - @classmethod - def Read_Picklez(cls, fname, *args, **kwds): - """Reads a graph from compressed Python pickled format, uncompressing - it on-the-fly. - @param fname: the name of the file or a stream to read from. - @return: the created graph object. - """ - import pickle as pickle + if not isinstance(result, cls): + raise TypeError("unpickled object is not a %s" % cls.__name__) - if hasattr(fname, "read"): - # Probably a file or a file-like object - if isinstance(fname, gzip.GzipFile): - result = pickle.load(fname) - else: - result = pickle.load(gzip.GzipFile(mode="rb", fileobj=fname)) - else: - result = pickle.load(gzip.open(fname, "rb")) return result @classmethod - def Read_Picklez(cls, fname, *args, **kwds): + def Read_Picklez(cls, fname): """Reads a graph from compressed Python pickled format, uncompressing it on-the-fly. @@ -2456,8 +2423,10 @@ def Read_Picklez(cls, fname, *args, **kwds): result = pickle.load(gzip.GzipFile(mode="rb", fileobj=fname)) else: result = pickle.load(gzip.open(fname, "rb")) + if not isinstance(result, cls): raise TypeError("unpickled object is not a %s" % cls.__name__) + return result # pylint: disable-msg=C0301,C0323 @@ -2609,12 +2578,18 @@ def write_svg( print('', file=f) print( - "", + "", file=f, ) print(file=f) print( - ''.format(width, height), end=" ", file=f) @@ -2638,13 +2613,15 @@ def write_svg( print(" ', file=f) @@ -2653,7 +2630,8 @@ def write_svg( edge_color_dict[e_col] = "Arrow2Lend{0}".format(marker_index) print("", file=f) print( - '', + '', file=f, ) @@ -2669,7 +2647,9 @@ def write_svg( print("'.format( + ' '.format( vertex_size, vertex_size * 2, vidx, colors[vidx] ), file=f, @@ -2774,7 +2755,9 @@ def write_svg( font.metrics("linespace") + 2, ) print( - ' '.format( + ' '.format( vertex_width / 2.0, vertex_height / 2.0, vertex_width, @@ -2786,13 +2769,19 @@ def write_svg( ) print( - ' '.format( + ' '.format( vertex_size / 2.0, vidx, font_size ), file=f, ) print( - '{2}'.format( + '' + '{2}'.format( vertex_size / 2.0, vidx, str(labels[vidx]) ), file=f, @@ -2830,7 +2819,7 @@ def _identify_format(cls, filename): # It is most likely a file try: filename = filename.name - except: + except Exception: return None root, ext = os.path.splitext(filename) @@ -3034,9 +3023,9 @@ def DictList( @return: the graph that was constructed """ - def create_list_from_indices(l, n): + def create_list_from_indices(indices, n): result = [None] * n - for i, v in l: + for i, v in indices: result[i] = v return result @@ -3388,8 +3377,8 @@ def Incidence( result, types = cls._Incidence(matrix, directed, mode, multiple, *args, **kwds) result.vs["type"] = types if is_weighted: - weight_attr = "weight" if weighted == True else weighted - _, rows, columns = result.get_incidence() + weight_attr = "weight" if weighted is True else weighted + _, rows, _ = result.get_incidence() num_vertices_of_first_kind = len(rows) for edge in result.es: source, target = edge.tuple @@ -3637,9 +3626,9 @@ def bipartite_projection( """ superclass_meth = super(Graph, self).bipartite_projection - if which == False: + if which is False: which = 0 - elif which == True: + elif which is True: which = 1 if which != 0 and which != 1: which = -1 @@ -3841,13 +3830,13 @@ def __isub__(self, other): self.delete_vertices(other) else: return NotImplemented - elif isinstance(other, _igraph.Vertex): + elif isinstance(other, Vertex): self.delete_vertices(other) - elif isinstance(other, _igraph.VertexSeq): + elif isinstance(other, VertexSeq): self.delete_vertices(other) - elif isinstance(other, _igraph.Edge): + elif isinstance(other, Edge): self.delete_edges(other) - elif isinstance(other, _igraph.EdgeSeq): + elif isinstance(other, EdgeSeq): self.delete_edges(other) else: return NotImplemented @@ -3881,13 +3870,13 @@ def __sub__(self, other): return NotImplemented else: return result - elif isinstance(other, _igraph.Vertex): + elif isinstance(other, Vertex): result.delete_vertices(other) - elif isinstance(other, _igraph.VertexSeq): + elif isinstance(other, VertexSeq): result.delete_vertices(other) - elif isinstance(other, _igraph.Edge): + elif isinstance(other, Edge): result.delete_edges(other) - elif isinstance(other, _igraph.EdgeSeq): + elif isinstance(other, EdgeSeq): result.delete_edges(other) else: return NotImplemented @@ -3937,13 +3926,13 @@ def __coerce__(self, other): """ if isinstance(other, (int, tuple, list, str)): return self, other - if isinstance(other, _igraph.Vertex): + if isinstance(other, Vertex): return self, other - if isinstance(other, _igraph.VertexSeq): + if isinstance(other, VertexSeq): return self, other - if isinstance(other, _igraph.Edge): + if isinstance(other, Edge): return self, other - if isinstance(other, _igraph.EdgeSeq): + if isinstance(other, EdgeSeq): return self, other return NotImplemented @@ -4428,7 +4417,7 @@ def find(self, *args, **kwds): if args: # Selecting first based on positional arguments, then checking # the criteria specified by the (remaining) keyword arguments - vertex = _igraph.VertexSeq.find(self, *args) + vertex = _VertexSeq.find(self, *args) if not kwds: return vertex vs = self.graph.vs.select(vertex.index) @@ -4544,7 +4533,7 @@ def select(self, *args, **kwds): >>> edges = g.vs.select(bs_gt=10, bs_lt=30) @return: the new, filtered vertex sequence""" - vs = _igraph.VertexSeq.select(self, *args) + vs = _VertexSeq.select(self, *args) operators = { "lt": operator.lt, @@ -4676,7 +4665,7 @@ def find(self, *args, **kwds): if args: # Selecting first based on positional arguments, then checking # the criteria specified by the keyword arguments - edge = _igraph.EdgeSeq.find(self, *args) + edge = _EdgeSeq.find(self, *args) if not kwds: return edge es = self.graph.es.select(edge.index) @@ -4833,7 +4822,7 @@ def select(self, *args, **kwds): @return: the new, filtered edge sequence """ - es = _igraph.EdgeSeq.select(self, *args) + es = _EdgeSeq.select(self, *args) is_directed = self.graph.is_directed() def _ensure_set(value): @@ -4947,8 +4936,8 @@ def _ensure_set(value): and e.target in value ] else: - # Optimized version when the edge sequence contains all the edges - # exactly once in increasing order of edge IDs + # Optimized version when the edge sequence contains all + # the edges exactly once in increasing order of edge IDs filtered_idxs = [ i for i in candidates @@ -4981,8 +4970,8 @@ def _ensure_set(value): or (e.target in set1 and e.source in set2) ] else: - # Optimized version when the edge sequence contains all the edges - # exactly once in increasing order of edge IDs + # Optimized version when the edge sequence contains all + # the edges exactly once in increasing order of edge IDs filtered_idxs = [ i for i in candidates @@ -5223,7 +5212,7 @@ def autocurve(graph, attribute="curved", default=0): multiplicities[u, v].append(edge.index) result = [default] * graph.ecount() - for pair, eids in multiplicities.items(): + for eids in multiplicities.values(): # Is it a single edge? if len(eids) < 2: continue @@ -5322,5 +5311,5 @@ def summary(obj, stream=None, *args, **kwds): stream.write("\n") -config = configuration.init() +config = init_configuration() del construct_graph_from_formula From 568958b43c7687149bee13170870ffade92f04a4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 28 May 2021 12:05:15 +0200 Subject: [PATCH 0291/1681] chore: updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9edadb98..1e5c555e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ * Updated igraph dependency to 0.9.3. +### Fixed + +* Improved performance of `Graph.from_networkx()` and `Graph.from_graph_tool()` + on large graphs, thanks to @szhorvat and @iosonofabio for fixing the issue. + ### Deprecated * Functions and methods that take string arguments that represent an underlying From a7d4ca18f7a901d4d7509ba34e31d5b0aa2e8a58 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 28 May 2021 12:14:51 +0200 Subject: [PATCH 0292/1681] refactor: moved the summary function to igraph.summary so PyDoctor generates nicer filenames in the API docs --- src/igraph/__init__.py | 21 +-------------------- src/igraph/summary.py | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 7a55a9eb9..e7bca8a96 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -125,7 +125,7 @@ quantile, power_law_fit, ) -from igraph.summary import GraphSummary +from igraph.summary import GraphSummary, summary from igraph.utils import ( dbl_epsilon, multidict, @@ -5292,24 +5292,5 @@ def write(graph, filename, *args, **kwds): save = write -def summary(obj, stream=None, *args, **kwds): - """Prints a summary of object o to a given stream - - Positional and keyword arguments not explicitly mentioned here are passed - on to the underlying C{summary()} method of the object if it has any. - - @param obj: the object about which a human-readable summary is requested. - @param stream: the stream to be used. If C{None}, the standard output - will be used. - """ - if stream is None: - stream = sys.stdout - if hasattr(obj, "summary"): - stream.write(obj.summary(*args, **kwds)) - else: - stream.write(str(obj)) - stream.write("\n") - - config = init_configuration() del construct_graph_from_formula diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 24fdf1113..27229dc66 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -8,7 +8,7 @@ from texttable import Texttable from textwrap import TextWrapper -__all__ = ("GraphSummary",) +__all__ = ("GraphSummary", "summary") __license__ = """\ Copyright (C) 2006-2012 Tamás Nepusz @@ -373,3 +373,22 @@ def __str__(self): return "\n".join("\n".join(self.wrapper.wrap(line)) for line in output) return "\n".join(output) + + +def summary(obj, stream=None, *args, **kwds): + """Prints a summary of object o to a given stream + + Positional and keyword arguments not explicitly mentioned here are passed + on to the underlying C{summary()} method of the object if it has any. + + @param obj: the object about which a human-readable summary is requested. + @param stream: the stream to be used. If C{None}, the standard output + will be used. + """ + if stream is None: + stream = sys.stdout + if hasattr(obj, "summary"): + stream.write(obj.summary(*args, **kwds)) + else: + stream.write(str(obj)) + stream.write("\n") From 58975cb05a7d4f1ef8c170833de65a0fb9cae326 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 28 May 2021 12:35:20 +0200 Subject: [PATCH 0293/1681] doc: added Dash docset generator --- scripts/mkdoc.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 96826951c..71888b4ea 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -50,5 +50,26 @@ echo "Generating HTML documentation..." # PDF not supported by PyDoctor +DOC2DASH=`which doc2dash 2>/dev/null || true` +if [ "x$DOC2DASH" != x ]; then + echo "Generating Dash docset..." + "$DOC2DASH" \ + --online-redirect-url "https://igraph.org/python/doc/api" \ + --name "python-igraph" \ + -d "${DOC_API_FOLDER}" \ + -f \ + "${DOC_API_FOLDER}/html" + DASH_READY=1 +else + echo "WARNING: doc2dash not installed, skipping Dash docset generation." + DASH_READY=0 +fi + +echo "" +echo "HTML API documentation generated in ${DOC_API_FOLDER}/html" +if [ "x${DASH_READY}" = x1 ]; then + echo "Dash docset generated in ${DOC_API_FOLDER}/python-igraph.docset" +fi + cd "$PWD" From f8a3d04b5036a964f1c980fc427d5c34467457e6 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 28 May 2021 10:29:14 +1000 Subject: [PATCH 0294/1681] Speed up from_graph_tool and clean up a little --- src/igraph/__init__.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index e7bca8a96..45b59393d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1906,9 +1906,6 @@ def from_networkx(cls, g): @param g: networkx Graph or DiGraph """ - import networkx as nx - from collections import defaultdict - # Graph attributes gattr = dict(g.graph) @@ -1931,7 +1928,7 @@ def from_networkx(cls, g): # Edges and edge attributes eattr_names = {name for (_, _, data) in g.edges.data() for name in data} - eattr = defaultdict(list) + eattr = {name: [] for name in eattr_names} edges = [] for (u, v, data) in g.edges.data(): edges.append((vd[u], vd[v])) @@ -2029,13 +2026,19 @@ def from_graph_tool(cls, g): for i in range(vcount): graph.vs[i][key] = prop[i] - # Edges - # NOTE: the order the edges are put in is necessary to set the - # attributes later on + # Edges and edge attributes + # NOTE: graph-tool is quite strongly typed, so each property is always + # defined for all edges, using default values for the type. E.g. for a + # string property/attribute the missing edges get an empty string. + edges = [] + eattr_names = list(g.edge_properties) + eattr = {name: [] for name in eattr_names} for e in g.edges(): - edge = graph.add_edge(int(e.source()), int(e.target())) - for key, val in list(g.edge_properties.items()): - edge[key] = val[e] + edges.append((int(e.source()), int(e.target()))) + for name, attr_map in list(g.edge_properties.items()): + eattr[name].append(attr_map[e]) + + graph.add_edges(edges, eattr) return graph From 6915ff6b0d3ccc4e60b0096626456263339efc32 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 28 May 2021 20:19:50 +1000 Subject: [PATCH 0295/1681] Skip list constructor --- src/igraph/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 45b59393d..606b2d3d1 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2021,7 +2021,7 @@ def from_graph_tool(cls, g): graph = cls(n=vcount, directed=g.is_directed(), graph_attrs=gattr) # Node attributes - for key, val in list(g.vertex_properties.items()): + for key, val in g.vertex_properties.items(): prop = val.get_array() for i in range(vcount): graph.vs[i][key] = prop[i] @@ -2035,7 +2035,7 @@ def from_graph_tool(cls, g): eattr = {name: [] for name in eattr_names} for e in g.edges(): edges.append((int(e.source()), int(e.target()))) - for name, attr_map in list(g.edge_properties.items()): + for name, attr_map in g.edge_properties.items(): eattr[name].append(attr_map[e]) graph.add_edges(edges, eattr) From 0c0ebd77907a2be6373476dae0bbfc754f419f6b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 28 May 2021 14:02:45 +0200 Subject: [PATCH 0296/1681] feat: added Graph.to_networkx(create_using=...) --- CHANGELOG.md | 3 +++ src/igraph/__init__.py | 25 +++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5c555e4..f6fd715fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ * `Graph.to_directed()` now supports a `mode=...` keyword argument. +* Added a `create_using=...` keyword argument to `Graph.to_networkx()` to + let the user specify which NetworkX class to use when converting the graph. + ### Changed * Updated igraph dependency to 0.9.3. diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 606b2d3d1..a32dfdcee 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1867,21 +1867,24 @@ def maximum_bipartite_matching(self, types="type", weights=None, eps=None): ############################################# # Auxiliary I/O functions - def to_networkx(self): - """Converts the graph to networkx format""" + def to_networkx(self, create_using=None): + """Converts the graph to networkx format. + + @param create_using: specifies which NetworkX graph class to use when + constructing the graph. C{None} means to let igraph infer the most + appropriate class based on whether the graph is directed and whether + it has multi-edges. + """ import networkx as nx # Graph: decide on directness and mutliplicity - if any(self.is_multiple()): - if self.is_directed(): - cls = nx.MultiDiGraph + if create_using is None: + if self.has_multiple(): + cls = nx.MultiDiGraph if self.is_directed() else nx.MultiGraph else: - cls = nx.MultiGraph + cls = nx.DiGraph if self.is_directed() else nx.Graph else: - if self.is_directed(): - cls = nx.DiGraph - else: - cls = nx.Graph + cls = create_using # Graph attributes kw = {x: self[x] for x in self.attributes()} @@ -1889,6 +1892,8 @@ def to_networkx(self): # Nodes and node attributes for i, v in enumerate(self.vs): + # TODO: use _nx_name if the attribute is present so we can achieve + # a lossless round-trip in terms of vertex names g.add_node(i, **v.attributes()) # Edges and edge attributes From d12808c6fac7b231128b86d8640ccc85c907fee1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 12:14:17 +0200 Subject: [PATCH 0297/1681] fix: fix comments in mkdoc.sh -- it takes no arguments now [ci skip] --- scripts/mkdoc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 71888b4ea..b49ffc3aa 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -2,7 +2,7 @@ # # Creates the API documentation for igraph's Python interface using PyDoctor # -# Usage: ./mkdoc.sh [--sync] [directory] +# Usage: ./mkdoc.sh SCRIPTS_FOLDER=`dirname $0` From c8997ad1c4825a3469341322036a9784be665436 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 31 May 2021 19:52:39 +1000 Subject: [PATCH 0298/1681] A bit more docs for matplotlib plotting --- doc/source/tutorial.rst | 17 +++++++++++++---- src/igraph/drawing/__init__.py | 4 ++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index d72d0ee26..6564c9b67 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -435,7 +435,7 @@ degree or betweenness centrality. You can do that with the tools presented so fa some basic Python knowledge, but since it is a common task to select vertices and edges based on attributes or structural properties, |igraph| gives you an easier way to do that: ->>> g.vs.select(_degree = g.maxdegree())["name"] +>>> g.vs.select(_degree=g.maxdegree())["name"] ["Alice", "Bob"] The syntax may seem a little bit awkward for the first sight, so let's try to interpret @@ -741,7 +741,7 @@ For instance, we can plot our imaginary social network with the Kamada-Kawai layout algorithm as follows: >>> layout = g.layout("kk") ->>> plot(g, layout = layout) +>>> plot(g, layout=layout) This should open an external image viewer showing a visual representation of the network, something like the one on the following figure (although the exact placement of @@ -753,6 +753,13 @@ nodes may be different on your machine since the layout is not deterministic): Our social network with the Kamada-Kawai layout algorithm +If you prefer to use `matplotlib`_ as a default plotting engine, create an axes and use +the ``target`` argument: + +>>> import matplotlib.pyplot as plt +>>> fig, ax = plt.subplots() +>>> plot(g, layout=layout, target=ax) + Hmm, this is not too pretty so far. A trivial addition would be to use the names as the vertex labels and to color the vertices according to the gender. Vertex labels are taken from the ``label`` attribute by default and vertex colors are determined by the @@ -761,7 +768,8 @@ from the ``label`` attribute by default and vertex colors are determined by the >>> g.vs["label"] = g.vs["name"] >>> color_dict = {"m": "blue", "f": "pink"} >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] ->>> plot(g, layout = layout, bbox = (300, 300), margin = 20) +>>> plot(g, layout=layout, bbox=(300, 300), margin=20) +>>> plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib version Note that we are simply re-using the previous layout object here, but we also specified that we need a smaller plot (300 x 300 pixels) and a larger margin around the graph @@ -777,7 +785,7 @@ Instead of specifying the visual properties as vertex and edge attributes, you c also give them as keyword arguments to :func:`plot`: >>> color_dict = {"m": "blue", "f": "pink"} ->>> plot(g, layout = layout, vertex_color = [color_dict[gender] for gender in g.vs["gender"]]) +>>> plot(g, layout=layout, vertex_color=[color_dict[gender] for gender in g.vs["gender"]]) This latter approach is preferred if you want to keep the properties of the visual representation of your graph separate from the graph itself. You can simply set up @@ -1053,3 +1061,4 @@ out immediately. .. _API documentation: https://igraph.org/python/doc/api/index.html .. _Graph class: https://igraph.org/python/doc/api/igraph.Graph.html .. _Discourse group: https://igraph.discourse.group +.. _matplotlib: https://matplotlib.org/ diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 8c9723b40..372b8bac0 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -423,6 +423,10 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @param target: the target where the object should be plotted. It can be one of the following types: + - C{matplotib.axes.Axes} -- a matplotlib/pyplot axes in which the + graph will be plotted. Drawing, saving to file, etc. is mostly + delegated to the chosen matplotlib backend. + - C{string} -- a file with the given name will be created and an appropriate Cairo surface will be attached to it. The supported image formats are: PNG, PDF, SVG and PostScript. From 75e0c5fe18dadb73a9e5e4edcf1bacc3b2110c8a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 31 May 2021 20:18:26 +1000 Subject: [PATCH 0299/1681] Improvements in the wording --- doc/source/tutorial.rst | 4 ++-- src/igraph/drawing/__init__.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 6564c9b67..17a584f49 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -753,8 +753,8 @@ nodes may be different on your machine since the layout is not deterministic): Our social network with the Kamada-Kawai layout algorithm -If you prefer to use `matplotlib`_ as a default plotting engine, create an axes and use -the ``target`` argument: +If you prefer to use `matplotlib`_ as a plotting engine, create an axes and use the +``target`` argument: >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 372b8bac0..db34f5a0c 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -424,8 +424,9 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): of the following types: - C{matplotib.axes.Axes} -- a matplotlib/pyplot axes in which the - graph will be plotted. Drawing, saving to file, etc. is mostly - delegated to the chosen matplotlib backend. + graph will be plotted. Drawing is delegated to the chosen matplotlib + backend, and you can use interactive backends and matplotlib + functions to save to file as well. - C{string} -- a file with the given name will be created and an appropriate Cairo surface will be attached to it. The supported image From 420728378bb70225a30985f15b08eb1e609d9e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Fri, 28 May 2021 14:22:35 +0200 Subject: [PATCH 0300/1681] docs: clarify add_edge() performance --- src/igraph/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index a32dfdcee..70681c303 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -344,13 +344,17 @@ def add_edge(self, source, target, **kwds): Keyword arguments (except the source and target arguments) will be assigned to the edge as attributes. + + The performance cost of adding a single edge or several edges + to a graph is similar. Thus, when adding several edges, a single + C{add_edges()} call is more efficient than multiple C{add_edge()} calls. @param source: the source vertex of the edge or its name. @param target: the target vertex of the edge or its name. @return: the newly added edge as an L{Edge} object. Use C{add_edges([(source, target)])} if you don't need the L{Edge} - object and want to avoid the overhead of creating t. + object and want to avoid the overhead of creating it. """ eid = self.ecount() self.add_edges([(source, target)]) From d53b9bc818122a0dd7cdf068090f717990808d78 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 13:02:34 +0200 Subject: [PATCH 0301/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index a4be06863..05e9977f1 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit a4be0686333f2e9fe38b3bc7a23b9073e848f77a +Subproject commit 05e9977f117821af26fe4b0dda0653d9d948c488 From 4fffe2e2c7e3474c5ea70c0aa50594e9b49d36a3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 13:57:52 +0200 Subject: [PATCH 0302/1681] fix: deprecate UbiGraphDrawer, list MatplotlibGraphDrawer in docstrings instead --- src/igraph/drawing/graph.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index d46b45538..5a70ce8af 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -4,7 +4,7 @@ This module contains routines to draw graphs on: - Cairo surfaces (L{DefaultGraphDrawer}) - - UbiGraph displays (L{UbiGraphDrawer}, see U{http://ubietylab.net/ubigraph}) + - Matplotlib axes (L{MatplotlibGraphDrawer}) It also contains routines to send an igraph graph directly to (U{Cytoscape}) using the @@ -34,7 +34,7 @@ from igraph.drawing.vertex import DefaultVertexDrawer from igraph.layout import Layout -__all__ = ("DefaultGraphDrawer", "UbiGraphDrawer", "CytoscapeGraphDrawer") +__all__ = ("DefaultGraphDrawer", "MatplotlibGraphDrawer", "CytoscapeGraphDrawer", "UbiGraphDrawer") __license__ = "GPL" cairo = find_cairo() @@ -542,6 +542,11 @@ class UbiGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): The drawer also has two attributes, C{vertex_defaults} and C{edge_defaults}. These are dictionaries that can be used to set default values for the vertex/edge attributes in Ubigraph. + + @deprecated: UbiGraph has not received updates since 2008 and is now not + available for download (at least not from the official sources). + The UbiGraph graph drawer will be removed from python-igraph in + 0.10.0. """ def __init__(self, url="http://localhost:20738/RPC2"): @@ -551,6 +556,11 @@ def __init__(self, url="http://localhost:20738/RPC2"): self.vertex_defaults = dict(color="#ff0000", shape="cube", size=1.0) self.edge_defaults = dict(color="#ffffff", width=1.0) + warn( + "UbiGraphDrawer is deprecated from python-igraph 0.9.4", + DeprecationWarning + ) + def draw(self, graph, *args, **kwds): """Draws the given graph on an UbiGraph display. From 89a8b9dc948d79443d2ac51c3e6b6046e94e3b2c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 13:58:42 +0200 Subject: [PATCH 0303/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6fd715fe..6a7910dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ * `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. +* `igraph.graph.drawing.UbiGraphDrawer` is deprecated as the upstream project + is not maintained since 2008. ## 0.9.1 From 56dc1b8dc3291fb4ac83a3da8f63588360bf20cd Mon Sep 17 00:00:00 2001 From: Casper van Elteren Date: Mon, 31 May 2021 13:58:58 +0200 Subject: [PATCH 0304/1681] fixed autocurve mpl (#406) * fixed autocurve mpl * fix: make sure that edge_curved=... is also respected when calling plot() Co-authored-by: Tamas Nepusz --- src/igraph/drawing/graph.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 5a70ce8af..c3302c06f 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -1190,7 +1190,13 @@ def callback_edge_offset(event): if default is True: default = 0.5 default = float(default) - kwds["edge_curved"] = autocurve(graph, attribute=None, default=default) + ecurved = autocurve(graph, attribute=None, default=default) + elif "edge_curved" in kwds: + ecurved = kwds["edge_curved"] + elif "curved" in graph.edge_attributes(): + ecurved = graph.es["curved"] + else: + ecurved = [0] * ne # Arrow style for directed and undirected graphs if graph.is_directed(): @@ -1204,7 +1210,6 @@ def callback_edge_offset(event): # Edge coordinates and curvature nloops = [0 for x in range(ne)] - has_curved = "curved" in graph.es.attributes() arrows = [] for ie, edge in enumerate(graph.es): src, tgt = edge.source, edge.target @@ -1284,14 +1289,14 @@ def callback_edge_offset(event): ) else: - curved = edge["curved"] if has_curved else False + curved = ecurved[ie] if curved: - aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + aux1 = (2 * x1 + x2) / 3.0 - curved * 0.5 * (y2 - y1), ( 2 * y1 + y2 - ) / 3.0 + edge.curved * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + ) / 3.0 + curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - curved * 0.5 * (y2 - y1), ( y1 + 2 * y2 - ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + ) / 3.0 + curved * 0.5 * (x2 - x1) start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) From 0b74f994ceaf15ae47539167f5faadd291e656c0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 13:59:38 +0200 Subject: [PATCH 0305/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7910dab..8f56df502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ * Improved performance of `Graph.from_networkx()` and `Graph.from_graph_tool()` on large graphs, thanks to @szhorvat and @iosonofabio for fixing the issue. +* Fixed the `autocurve=...` keyword argument of `plot()` when using the + Matplotlib backend. + ### Deprecated * Functions and methods that take string arguments that represent an underlying From 6267902388b22a17ef16ab4ada6911cdffbb07d9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 14:05:53 +0200 Subject: [PATCH 0306/1681] chore: removed __license__ vars from most of the modules, a single LICENSE file at the root is enough [ci skip] --- src/igraph/clustering.py | 20 -------------------- src/igraph/configuration.py | 20 -------------------- src/igraph/cut.py | 20 -------------------- src/igraph/datatypes.py | 20 -------------------- src/igraph/drawing/__init__.py | 2 -- src/igraph/drawing/colors.py | 20 -------------------- src/igraph/drawing/coord.py | 2 -- src/igraph/drawing/edge.py | 2 -- src/igraph/drawing/graph.py | 21 +++++++++++---------- src/igraph/drawing/shapes.py | 20 -------------------- src/igraph/drawing/text.py | 1 - src/igraph/drawing/utils.py | 1 - src/igraph/drawing/vertex.py | 1 - src/igraph/formula.py | 20 -------------------- src/igraph/layout.py | 20 -------------------- src/igraph/matching.py | 21 --------------------- src/igraph/operators.py | 20 -------------------- src/igraph/remote/gephi.py | 19 ------------------- src/igraph/sparse_matrix.py | 18 ------------------ src/igraph/statistics.py | 20 -------------------- src/igraph/utils.py | 19 ------------------- 21 files changed, 11 insertions(+), 296 deletions(-) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 3c00b3c95..37200d0f6 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -2,26 +2,6 @@ # -*- coding: utf-8 -*- """Classes related to graph clustering.""" -__license__ = u""" -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - from copy import deepcopy from math import pi from io import StringIO diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index f39a6cdd3..039012b6d 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -8,26 +8,6 @@ as well as saving them to and retrieving them from disk. """ -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - import sys if sys.version_info < (3, 2): diff --git a/src/igraph/cut.py b/src/igraph/cut.py index 2d7a5d590..c2b39b7da 100644 --- a/src/igraph/cut.py +++ b/src/igraph/cut.py @@ -4,26 +4,6 @@ from igraph.clustering import VertexClustering -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - class Cut(VertexClustering): """A cut of a given graph. diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index 75a7bae5f..ce6ee6990 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -4,26 +4,6 @@ from itertools import islice -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - class Matrix(object): """Simple matrix data type. diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index db34f5a0c..6e90aa247 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -38,8 +38,6 @@ __all__ = ("BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot") -__license__ = "GPL" - cairo = find_cairo() ##################################################################### diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index 049cfeb99..72e3e80ac 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -4,26 +4,6 @@ Color handling functions. """ -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - from igraph.datatypes import Matrix from igraph.utils import str_to_orientation from math import ceil diff --git a/src/igraph/drawing/coord.py b/src/igraph/drawing/coord.py index a5e51e622..57d742882 100644 --- a/src/igraph/drawing/coord.py +++ b/src/igraph/drawing/coord.py @@ -5,8 +5,6 @@ from igraph.drawing.baseclasses import AbstractCairoDrawer from igraph.drawing.utils import BoundingBox -__license__ = "GPL" - ##################################################################### diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 100cee5c6..f01c3e9a7 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -11,8 +11,6 @@ "TaperedEdgeDrawer", ) -__license__ = "GPL" - from igraph.drawing.colors import clamp from igraph.drawing.metamagic import AttributeCollectorBase from igraph.drawing.text import TextAlignment diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index c3302c06f..b47587b4f 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -13,8 +13,6 @@ network from Cytoscape and convert it to igraph format. """ -from collections import defaultdict - from math import atan2, cos, pi, sin, tan, sqrt from warnings import warn @@ -34,8 +32,10 @@ from igraph.drawing.vertex import DefaultVertexDrawer from igraph.layout import Layout -__all__ = ("DefaultGraphDrawer", "MatplotlibGraphDrawer", "CytoscapeGraphDrawer", "UbiGraphDrawer") -__license__ = "GPL" +__all__ = ( + "DefaultGraphDrawer", "MatplotlibGraphDrawer", "CytoscapeGraphDrawer", + "UbiGraphDrawer" +) cairo = find_cairo() @@ -442,7 +442,7 @@ def draw(self, graph, palette, *args, **kwds): # Intersection at bottom edge of label try: cx += h / tan(alpha) - except: + except Exception: pass # tan(alpha) == inf cy -= h elif alpha > gamma and alpha <= gamma + 2 * beta: @@ -453,7 +453,7 @@ def draw(self, graph, palette, *args, **kwds): # Intersection at top edge of label try: cx -= h / tan(alpha) - except: + except Exception: pass # tan(alpha) == inf cy += h # Draw the label @@ -913,10 +913,11 @@ class GephiGraphStreamingDrawer(AbstractGraphDrawer): The Gephi graph streaming format is a simple JSON-based format that can be used to post mutations to a graph (i.e. node and edge additions, removals and updates) - to a remote component. For instance, one can open up Gephi (U{http://www.gephi.org}), - install the Gephi graph streaming plugin and then send a graph from igraph - straight into the Gephi window by using C{GephiGraphStreamingDrawer} with the - appropriate URL where Gephi is listening. + to a remote component. For instance, one can open up Gephi + (U{http://www.gephi.org}), install the Gephi graph streaming plugin and then + send a graph from igraph straight into the Gephi window by using + C{GephiGraphStreamingDrawer} with the appropriate URL where Gephi is + listening. The C{connection} property exposes the L{GephiConnection} that the drawer uses. The drawer also has a property called C{streamer} which exposes the underlying diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index 0a3cd81a9..bb7eec3e5 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -19,26 +19,6 @@ __all__ = ("ShapeDrawerDirectory",) -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - from math import atan2, copysign, cos, pi, sin import sys diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index 6de3b3b9f..7d00624bd 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -8,7 +8,6 @@ from warnings import warn __all__ = ("TextAlignment", "TextDrawer") -__license__ = "GPL" __docformat__ = "restructuredtext en" diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 68f1b9b60..39a16e8fe 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -7,7 +7,6 @@ from operator import itemgetter __all__ = ("BoundingBox", "Point", "Rectangle") -__license__ = "GPL" ##################################################################### diff --git a/src/igraph/drawing/vertex.py b/src/igraph/drawing/vertex.py index 9f4d082a0..fbae32e3f 100644 --- a/src/igraph/drawing/vertex.py +++ b/src/igraph/drawing/vertex.py @@ -11,7 +11,6 @@ from math import pi __all__ = ("AbstractVertexDrawer", "AbstractCairoVertexDrawer", "DefaultVertexDrawer") -__license__ = "GPL" class AbstractVertexDrawer(AbstractDrawer): diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 2b1f26aa6..9b31b0aed 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -17,26 +17,6 @@ __all__ = ("construct_graph_from_formula",) -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - def generate_edges(formula): """Parses an edge specification from the head of the given diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 38bd3150c..eaf3e54c6 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -12,26 +12,6 @@ from igraph.drawing.utils import BoundingBox from igraph.statistics import RunningMean -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - class Layout(object): """Represents the layout of a graph. diff --git a/src/igraph/matching.py b/src/igraph/matching.py index 85d40d226..5ec97a456 100644 --- a/src/igraph/matching.py +++ b/src/igraph/matching.py @@ -2,29 +2,8 @@ # -*- coding: utf-8 -*- """Classes representing matchings on graphs.""" -from igraph.clustering import VertexClustering from igraph._igraph import Vertex -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - class Matching(object): """A matching of vertices in a graph. diff --git a/src/igraph/operators.py b/src/igraph/operators.py index 8fe113b5c..eaf5b0075 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -2,28 +2,8 @@ # -*- coding: utf-8 -*- """Implementation of union, disjoint union and intersection operators.""" - __all__ = ("disjoint_union", "union", "intersection") __docformat__ = "restructuredtext en" -__license__ = """ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" from igraph._igraph import GraphBase, _union, _intersection, _disjoint_union diff --git a/src/igraph/remote/gephi.py b/src/igraph/remote/gephi.py index 74b9d0d46..9ae527dba 100644 --- a/src/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -7,25 +7,6 @@ __all__ = ("GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat") __docformat__ = "restructuredtext en" -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" class GephiConnection(object): diff --git a/src/igraph/sparse_matrix.py b/src/igraph/sparse_matrix.py index 132d68bc4..bcf5436fe 100644 --- a/src/igraph/sparse_matrix.py +++ b/src/igraph/sparse_matrix.py @@ -6,24 +6,6 @@ __all__ = () __docformat__ = "restructuredtext en" -__license__ = u""" -Copyright (C) 2021 igraph development team - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" from operator import add from igraph._igraph import ( diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 951db6d9c..2a46e044e 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -4,26 +4,6 @@ Statistics related stuff in igraph """ -__license__ = """\ -Copyright (C) 2006-2012 Tamas Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - import math __all__ = ( diff --git a/src/igraph/utils.py b/src/igraph/utils.py index 085f4a5ff..4b86e5dd6 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -22,25 +22,6 @@ ) __docformat__ = "restructuredtext en" -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" def _is_running_in_ipython(): From 6fe65c8b9c05f35777a3e4ec3dc8f637332afd1d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 14:21:56 +0200 Subject: [PATCH 0307/1681] style: fix flake8 linter warnings --- src/igraph/__init__.py | 2 +- src/igraph/app/shell.py | 7 +++---- src/igraph/clustering.py | 6 +++--- src/igraph/datatypes.py | 6 +++--- src/igraph/drawing/__init__.py | 1 - src/igraph/drawing/colors.py | 10 +++++----- src/igraph/drawing/edge.py | 13 ++++++++----- src/igraph/drawing/metamagic.py | 1 + src/igraph/drawing/utils.py | 4 +++- src/igraph/formula.py | 9 ++++++--- src/igraph/remote/gephi.py | 4 +++- src/igraph/statistics.py | 3 ++- src/igraph/summary.py | 23 ++--------------------- 13 files changed, 40 insertions(+), 49 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 70681c303..0944b6c32 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -344,7 +344,7 @@ def add_edge(self, source, target, **kwds): Keyword arguments (except the source and target arguments) will be assigned to the edge as attributes. - + The performance cost of adding a single edge or several edges to a graph is similar. Thus, when adding several edges, a single C{add_edges()} call is more efficient than multiple C{add_edge()} calls. diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index 6defa2449..0b049d5fb 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -23,9 +23,8 @@ import re import sys -# pylint: disable-msg=W0401 -# W0401: wildcard import. That's exactly what we need for the shell. -from igraph import __version__, set_progress_handler, set_status_handler +from igraph import __version__ +from igraph._igraph import set_progress_handler, set_status_handler from igraph.configuration import Configuration @@ -193,7 +192,7 @@ def render(self, template): the corresponding terminal control string (if it's defined) or '' (if it's not). """ - return re.sub("r\$\$|\${\w+}", self._render_sub, template) + return re.sub("r\$\$|\${\w+}", self._render_sub, template) # noqa: W605 def _render_sub(self, match): """Helper function for L{render}""" diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 37200d0f6..644c06873 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -383,7 +383,7 @@ def _recalculate_modularity_safe(self): """ try: return self.recalculate_modularity() - except: + except Exception: return None finally: self._modularity_dirty = False @@ -1467,7 +1467,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): if "mark_groups" not in kwds: if Configuration.instance()["plotting.mark_groups"]: prepare_groups = True - elif kwds["mark_groups"] == True: + elif kwds["mark_groups"] is True: prepare_groups = True if prepare_groups: @@ -1501,7 +1501,7 @@ def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): # Lists, tuples try: first = mark_groups[0] - except: + except Exception: # Hmm. Maybe not a list or tuple? first = None if first is not None: diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index ce6ee6990..6dab7223a 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -351,9 +351,9 @@ def __plot__(self, context, bbox, palette, **kwds): col_names = [str(name) for name in islice(col_names, self._ncol)] if len(col_names) < self._ncol: col_names.extend([""] * (self._ncol - len(col_names))) - if values == False: + if values is False: values = None - if values == True: + if values is True: values = self if isinstance(values, list): values = Matrix(list) @@ -428,7 +428,7 @@ def __plot__(self, context, bbox, palette, **kwds): # Draw matrix x, y = ox, oy if style is None: - fill = lambda: None + fill = lambda: None # noqa: E731 else: fill = context.fill_preserve for row in self: diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 6e90aa247..c428f62e7 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -16,7 +16,6 @@ """ -from io import StringIO from warnings import warn import os diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index 72e3e80ac..4532fb173 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -339,13 +339,13 @@ def _get(self, v): class PrecalculatedPalette(Palette): """A palette that returns colors from a pre-calculated list of colors""" - def __init__(self, l): + def __init__(self, items): """Creates the palette backed by the given list. The list must contain RGBA quadruplets or color names, which will be resolved first by L{color_name_to_rgba()}. Anything that is understood by L{color_name_to_rgba()} is OK here.""" - Palette.__init__(self, len(l)) - for idx, color in enumerate(l): + Palette.__init__(self, len(items)) + for idx, color in enumerate(items): if isinstance(color, str): color = color_name_to_rgba(color) self._cache[idx] = color @@ -555,7 +555,7 @@ def darken(color, ratio=0.5): return (red * ratio, green * ratio, blue * ratio, alpha) -def hsla_to_rgba(h, s, l, alpha=1.0): +def hsla_to_rgba(h, s, l, alpha=1.0): # noqa: E741 """Converts a color given by its HSLA coordinates (hue, saturation, lightness, alpha) to RGBA coordinates. @@ -584,7 +584,7 @@ def hsla_to_rgba(h, s, l, alpha=1.0): return (c + m, m, x + m, alpha) -def hsl_to_rgb(h, s, l): +def hsl_to_rgb(h, s, l): # noqa: E741 """Converts a color given by its HSL coordinates (hue, saturation, lightness) to RGB coordinates. diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index f01c3e9a7..95e87b1c2 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -243,7 +243,6 @@ def intersect_bezier_circle( t0 = 1.0 t1 = 1.0 - radius / source_target_distance - xt0, yt0 = x3, y3 xt1, yt1 = bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) distance_t0 = 0 @@ -254,7 +253,8 @@ def intersect_bezier_circle( t_new = (t0 + t1) / 2.0 else: if abs(distance_t1 - radius) < abs(distance_t0 - radius): - # If t1 gets us closer to the circumference step in the same direction + # If t1 gets us closer to the circumference step in the + # same direction t_new = t1 + (t1 - t0) / 2.0 else: t_new = t1 - (t1 - t0) @@ -327,8 +327,10 @@ def intersect_bezier_circle( x_arrow_mid - x_src ) - # Offset the second control point (aux2) such that it falls precisely on the normal to the arrow base vector - # Strictly speaking, offset_length is the offset length divided by the length of the arrow base vector. + # Offset the second control point (aux2) such that it falls precisely + # on the normal to the arrow base vector. Strictly speaking, + # offset_length is the offset length divided by the length of the + # arrow base vector. offset_length = (x_arrow_mid - aux2[0]) * x_arrow_base_vec + ( y_arrow_mid - aux2[1] ) * y_arrow_base_vec @@ -341,7 +343,8 @@ def intersect_bezier_circle( aux2[1] + y_arrow_base_vec * offset_length, ) - # Draw tthe curve from the first vertex to the midpoint of the base of the arrow head + # Draw the curve from the first vertex to the midpoint of the base + # of the arrow head ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], x_arrow_mid, y_arrow_mid) else: # Determine where the edge intersects the circumference of the diff --git a/src/igraph/drawing/metamagic.py b/src/igraph/drawing/metamagic.py index 164aef3a9..150e1ceda 100644 --- a/src/igraph/drawing/metamagic.py +++ b/src/igraph/drawing/metamagic.py @@ -67,6 +67,7 @@ class VisualEdgeBuilder(AttributeCollectorBase): __all__ = ("AttributeSpecification", "AttributeCollectorBase") + # pylint: disable-msg=R0903 # R0903: too few public methods class AttributeSpecification(object): diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 39a16e8fe..051d34ede 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -462,7 +462,9 @@ def find_matplotlib(): if has_mpl: import matplotlib.pyplot as plt else: - plt = FakeModule("You need to install matplotlib.pyplot to use this functionality") + plt = FakeModule( + "You need to install matplotlib.pyplot to use this functionality" + ) return mpl, plt diff --git a/src/igraph/formula.py b/src/igraph/formula.py index 9b31b0aed..ad5473d15 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -23,8 +23,10 @@ def generate_edges(formula): formula part and yields the following: - startpoint(s) of the edge by vertex names - - endpoint(s) of the edge by names or an empty list if the vertices are isolated - - a pair of bools to denote whether we had arrowheads at the start and end vertices + - endpoint(s) of the edge by names or an empty list if the vertices are + isolated + - a pair of bools to denote whether we had arrowheads at the start and end + vertices """ if formula == "": yield [], [""], [False, False] @@ -72,7 +74,8 @@ def generate_edges(formula): pass else: msg = ( - "invalid token found in edge specification: %s; token_type=%r; tok=%r" + "invalid token found in edge specification: %s; " + "token_type=%r; tok=%r" % (formula, token_type, tok) ) raise SyntaxError(msg) diff --git a/src/igraph/remote/gephi.py b/src/igraph/remote/gephi.py index 9ae527dba..c75651935 100644 --- a/src/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -3,7 +3,9 @@ """Classes that help igraph communicate with Gephi (http://www.gephi.org).""" import json -import urllib.request, urllib.error, urllib.parse +import urllib.error +import urllib.parse +import urllib.request __all__ = ("GephiConnection", "GephiGraphStreamer", "GephiGraphStreamingAPIFormat") __docformat__ = "restructuredtext en" diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 2a46e044e..c1e1be2cf 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -25,7 +25,8 @@ class FittedPowerLaw(object): >>> result = power_law_fit([1, 2, 3, 4, 5, 6]) >>> result # doctest:+ELLIPSIS - FittedPowerLaw(continuous=False, alpha=2.42..., xmin=3.0, L=-7.54..., D=0.21..., p=0.993...) + FittedPowerLaw(continuous=False, alpha=2.42..., xmin=3.0, L=-7.54..., \ +D=0.21..., p=0.993...) >>> print(result) # doctest:+ELLIPSIS Fitted power-law distribution on discrete data diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 27229dc66..022f50479 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- """Summary representation of a graph.""" +import sys + from igraph.statistics import median from itertools import islice from math import ceil @@ -10,26 +12,6 @@ __all__ = ("GraphSummary", "summary") -__license__ = """\ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary - -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 2 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, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -02110-1301 USA -""" - class FakeWrapper(object): """Object whose interface is compatible with C{textwrap.TextWrapper} @@ -143,7 +125,6 @@ def _construct_edgelist_adjlist(self): """Constructs the part in the summary that prints the edge list in an adjacency list format.""" result = [self._edges_header] - arrow = self._arrow_format if self._graph.vcount() == 0: return From c499db202663ed1bc4736ab62e30a1395713dffe Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 14:24:13 +0200 Subject: [PATCH 0308/1681] style: removed unneeded pylint linter comments as we use flake8 now --- src/igraph/__init__.py | 4 ---- src/igraph/app/shell.py | 7 ------- src/igraph/clustering.py | 4 ---- src/igraph/configuration.py | 2 -- src/igraph/cut.py | 3 --- src/igraph/datatypes.py | 6 ------ src/igraph/drawing/__init__.py | 4 ---- src/igraph/drawing/baseclasses.py | 4 ---- src/igraph/drawing/coord.py | 2 -- src/igraph/drawing/graph.py | 9 --------- src/igraph/drawing/metamagic.py | 4 ---- src/igraph/drawing/shapes.py | 3 --- src/igraph/drawing/utils.py | 6 ------ src/igraph/statistics.py | 2 -- 14 files changed, 60 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 0944b6c32..adf896538 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -158,7 +158,6 @@ def deprecated(message): warn(message, DeprecationWarning, stacklevel=3) -# pylint: disable-msg=E1101 class Graph(GraphBase): """Generic graph. @@ -2441,9 +2440,6 @@ def Read_Picklez(cls, fname): return result - # pylint: disable-msg=C0301,C0323 - # C0301: line too long. - # C0323: operator not followed by a space - well, print >>f looks OK def write_svg( self, fname, diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index 0b049d5fb..e553cf69d 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -28,9 +28,6 @@ from igraph.configuration import Configuration -# pylint: disable-msg=C0103,R0903 -# C0103: invalid name. Disabled because this is a third-party class. -# R0903: too few public methods. class TerminalController: """ A class that can be used to portably generate formatted output to @@ -311,14 +308,12 @@ def supports_status_messages(self): called C{_status_handler}.""" return hasattr(self, "_status_handler") - # pylint: disable-msg=E1101 def get_progress_handler(self): """Returns the progress handler (if exists) or None (if not).""" if self.supports_progress_bar(): return self._progress_handler return None - # pylint: disable-msg=E1101 def get_status_handler(self): """Returns the status handler (if exists) or None (if not).""" if self.supports_status_messages(): @@ -524,8 +519,6 @@ def main(): shell = None for shell_class in shell_classes: - # pylint: disable-msg=W0703 - # W0703: catch "Exception" try: shell = shell_class() break diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 644c06873..665fc9f3a 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -246,7 +246,6 @@ def __init__( else: self._modularity_params = dict(modularity_params) - # pylint: disable-msg=C0103 @classmethod def FromAttribute(cls, graph, attribute, intervals=None, params=None): """Creates a vertex clustering based on the value of a vertex attribute. @@ -738,7 +737,6 @@ def _item_box_size(self, context, horiz, idx): return x_advance - x_bearing, height return height, x_advance - x_bearing - # pylint: disable-msg=R0913 def _plot_item(self, context, horiz, idx, x, y): """Plots a dendrogram item to the given Cairo context @@ -763,8 +761,6 @@ def _plot_item(self, context, horiz, idx, x, y): context.show_text(str(self._names[idx])) context.restore() - # pylint: disable-msg=C0103,W0613 - # W0613 = unused argument 'palette' def __plot__(self, context, bbox, palette, *args, **kwds): """Draws the dendrogram on the given Cairo context diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 039012b6d..edfb00815 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -183,8 +183,6 @@ class Configuration(object): inline in IPython's console if the console supports it. Default: C{True} """ - # pylint: disable-msg=R0903 - # R0903: too few public methods class Types(object): """Static class for the implementation of custom getter/setter functions for configuration keys""" diff --git a/src/igraph/cut.py b/src/igraph/cut.py index c2b39b7da..0f49193bb 100644 --- a/src/igraph/cut.py +++ b/src/igraph/cut.py @@ -46,7 +46,6 @@ class Cut(VertexClustering): >>> mc.es["color"] = "red" """ - # pylint: disable-msg=R0913 def __init__(self, graph, value=None, cut=None, partition=None, partition2=None): """Initializes the cut. @@ -88,7 +87,6 @@ def __str__(self): self._value, ) - # pylint: disable-msg=C0103 @property def es(self): """Returns an edge selector restricted to the cut""" @@ -150,7 +148,6 @@ class Flow(Cut): >>> mf.es["color"] = "red" """ - # pylint: disable-msg=R0913 def __init__(self, graph, value, flow, cut, partition): """Initializes the flow. diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index 6dab7223a..2a4e31fde 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -25,7 +25,6 @@ def __init__(self, data=None): self._nrow, self._ncol, self._data = 0, 0, [] self.data = data - # pylint: disable-msg=C0103 @classmethod def Fill(cls, value, *args): """Creates a matrix filled with the given value @@ -47,7 +46,6 @@ def Fill(cls, value, *args): mtrx = [[value] * width for _ in range(height)] return cls(mtrx) - # pylint: disable-msg=C0103 @classmethod def Zero(cls, *args): """Creates a matrix filled with zeros. @@ -59,7 +57,6 @@ def Zero(cls, *args): result = cls.Fill(0, *args) return result - # pylint: disable-msg=C0103 @classmethod def Identity(cls, *args): """Creates an identity matrix. @@ -68,7 +65,6 @@ def Identity(cls, *args): two integers or a tuple. If a single integer is given here, the matrix is assumed to be square-shaped. """ - # pylint: disable-msg=W0212 result = cls.Fill(0, *args) for i in range(min(result.shape)): result._data[i][i] = 1 @@ -326,8 +322,6 @@ def __plot__(self, context, bbox, palette, **kwds): is square-shaped, the same names are used for both column and row names. """ - # pylint: disable-msg=W0142 - # pylint: disable-msg=C0103 grid_width = float(kwds.get("grid_width", 1.0)) border_width = float(kwds.get("border_width", 1.0)) style = kwds.get("style", "boolean") diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index c428f62e7..a59bb7f86 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -85,8 +85,6 @@ class Plot(object): added by the L{Plot.add} method and removed by the L{Plot.remove} method. """ - # pylint: disable-msg=E1101 - # E1101: Module 'cairo' has no 'foo' member - of course it has! :) def __init__(self, target=None, bbox=None, palette=None, background=None): """Creates a new plot. @@ -256,8 +254,6 @@ def mark_dirty(self): """Marks the plot as dirty (should be redrawn)""" self._is_dirty = True - # pylint: disable-msg=W0142 - # W0142: used * or ** magic def redraw(self, context=None): """Redraws the plot""" ctx = context or self._ctx diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index ab2c98dc7..ddbc9b9a4 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -8,8 +8,6 @@ ##################################################################### -# pylint: disable-msg=R0903 -# R0903: too few public methods class AbstractDrawer(object): """Abstract class that serves as a base class for anything that draws an igraph object.""" @@ -22,8 +20,6 @@ def draw(self, *args, **kwds): ##################################################################### -# pylint: disable-msg=R0903 -# R0903: too few public methods class AbstractCairoDrawer(AbstractDrawer): """Abstract class that serves as a base class for anything that draws on a Cairo context within a given bounding box. diff --git a/src/igraph/drawing/coord.py b/src/igraph/drawing/coord.py index 57d742882..fc0626bd8 100644 --- a/src/igraph/drawing/coord.py +++ b/src/igraph/drawing/coord.py @@ -8,8 +8,6 @@ ##################################################################### -# pylint: disable-msg=R0922 -# R0922: Abstract class is only referenced 1 times class CoordinateSystem(AbstractCairoDrawer): """Class implementing a coordinate system object. diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index b47587b4f..c8afe6f14 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -42,15 +42,10 @@ ##################################################################### -# pylint: disable-msg=R0903 -# R0903: too few public methods class AbstractGraphDrawer(AbstractDrawer): """Abstract class that serves as a base class for anything that draws an igraph.Graph.""" - # pylint: disable-msg=W0221 - # W0221: argument number differs from overridden method - # E1101: Module 'cairo' has no 'foo' member - of course it does :) def draw(self, graph, *args, **kwds): """Abstract method, must be implemented in derived classes.""" raise NotImplementedError("abstract class") @@ -223,10 +218,6 @@ def _determine_vertex_order(self, graph, kwds): return vertex_order - # pylint: disable-msg=W0142,W0221,E1101 - # W0142: Used * or ** magic - # W0221: argument number differs from overridden method - # E1101: Module 'cairo' has no 'foo' member - of course it does :) def draw(self, graph, palette, *args, **kwds): # Some abbreviations for sake of simplicity directed = graph.is_directed() diff --git a/src/igraph/drawing/metamagic.py b/src/igraph/drawing/metamagic.py index 150e1ceda..29bdd2f22 100644 --- a/src/igraph/drawing/metamagic.py +++ b/src/igraph/drawing/metamagic.py @@ -68,8 +68,6 @@ class VisualEdgeBuilder(AttributeCollectorBase): __all__ = ("AttributeSpecification", "AttributeCollectorBase") -# pylint: disable-msg=R0903 -# R0903: too few public methods class AttributeSpecification(object): """Class that describes how the value of a given attribute should be retrieved. @@ -342,8 +340,6 @@ def _collect_attributes(self, attr_spec, config=None): def __getitem__(self, index): """Returns the collected attributes of the vertex/edge with the given index.""" - # pylint: disable-msg=E1101 - # E1101: instance has no '_attributes' member return self._cache[index] def __len__(self): diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index bb7eec3e5..b81daf815 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -50,7 +50,6 @@ def draw_path(ctx, center_x, center_y, width, height=None): """ raise NotImplementedError("abstract class") - # pylint: disable-msg=W0613 @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the shape centered at (center_x, center_y) @@ -95,8 +94,6 @@ def draw_path(ctx, center_x, center_y, width, height=None): height = height or width ctx.rectangle(center_x - width / 2, center_y - height / 2, width, height) - # pylint: disable-msg=C0103, R0911 - # R0911: too many return statements @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): """Determines where the rectangle centered at (center_x, center_y) diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 051d34ede..31174fd73 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -400,8 +400,6 @@ def __or__(self, other): ##################################################################### -# pylint: disable-msg=R0903 -# R0903: too few public methods class FakeModule: """Fake module that raises an exception for everything""" @@ -482,8 +480,6 @@ def __new__(cls, x, y): """Creates a new point with the given coordinates""" return tuple.__new__(cls, (x, y)) - # pylint: disable-msg=W0622 - # W0622: redefining built-in 'len' @classmethod def _make(cls, iterable, new=tuple.__new__, len=len): """Creates a new point from a sequence or iterable""" @@ -500,8 +496,6 @@ def _asdict(self): """Returns a new dict which maps field names to their values""" return dict(zip(self._fields, self)) - # pylint: disable-msg=W0141 - # W0141: used builtin function 'map' def _replace(self, **kwds): """Returns a new point object replacing specified fields with new values""" diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index c1e1be2cf..7657ab6a4 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -179,7 +179,6 @@ def mean(self): """Returns the mean of the elements in the histogram""" return self._running_mean.mean - # pylint: disable-msg=C0103 @property def sd(self): """Returns the standard deviation of the elements in @@ -336,7 +335,6 @@ class RunningMean(object): the fly) """ - # pylint: disable-msg=C0103 def __init__(self, items=None, n=0.0, mean=0.0, sd=0.0): """RunningMean(items=None, n=0.0, mean=0.0, sd=0.0) From 4af8463fea4f7996b4ee717af40322ac8a152102 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 19:28:28 +0200 Subject: [PATCH 0309/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 05e9977f1..2ceb15db7 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 05e9977f117821af26fe4b0dda0653d9d948c488 +Subproject commit 2ceb15db7983a15e499844daddd1e1aa72cb0138 From 917b8acd1fa0ad6c768e97118d88fd3690c46271 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 19:29:40 +0200 Subject: [PATCH 0310/1681] chore: updated changelog, preparing for release [ci skip] --- CHANGELOG.md | 4 ++-- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f56df502..e9aa1aec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # igraph Python interface changelog -## 0.9.3 +## 0.9.4 ### Added @@ -18,7 +18,7 @@ ### Changed -* Updated igraph dependency to 0.9.3. +* Updated igraph dependency to 0.9.4. ### Fixed diff --git a/doc/source/conf.py b/doc/source/conf.py index 4eeb3ae08..7bb53614e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.9.1' +version = '0.9.4' # The full version, including alpha/beta/rc tags. -release = '0.9.1' +release = '0.9.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index 14ef391dc..c7f87fb0d 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 9, 1) +__version_info__ = (0, 9, 4) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 2d841de1da3a1749d6cdaeb6cfa2078e00202215 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 31 May 2021 19:43:14 +0200 Subject: [PATCH 0311/1681] chore: trigger CI to build artifacts From 2ccf9e9b88c37fc277dbd8eb8e0bce0a73e33cae Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 1 Jun 2021 14:40:23 +0200 Subject: [PATCH 0312/1681] build: force C++ linker during build by adding a dummy C++ file --- setup.py | 11 ++++++----- src/_igraph/force_cpp_linker.cpp | 4 ++++ 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 src/_igraph/force_cpp_linker.cpp diff --git a/setup.py b/setup.py index 5f2e4a5b0..9163cabd2 100644 --- a/setup.py +++ b/setup.py @@ -420,6 +420,11 @@ def run(self): extra_libraries = os.environ["IGRAPH_EXTRA_DYNAMIC_LIBRARIES"].split(',') buildcfg.libraries.extend(extra_libraries) + # Remove C++ standard library as we will use the C++ linker + for lib in ("c++", "stdc++"): + if lib in buildcfg.libraries: + buildcfg.libraries.remove(lib) + # Prints basic build information buildcfg.print_build_info() @@ -664,11 +669,6 @@ def process_args_from_command_line(self): def replace_static_libraries(self, only=None, exclusions=None): """Replaces references to libraries with full paths to their static versions if the static version is to be found on the library path.""" - building_on_windows = building_on_windows_msvc() - - if not building_on_windows and "stdc++" not in self.libraries: - self.libraries.append("stdc++") - if exclusions is None: exclusions = [] @@ -757,6 +757,7 @@ def use_educated_guess(self): # Define the extension sources = glob.glob(os.path.join("src", "_igraph", "*.c")) +sources.append(os.path.join("src", "_igraph", "force_cpp_linker.cpp")) igraph_extension = Extension("igraph._igraph", sources) description = """Python interface to the igraph high performance graph diff --git a/src/_igraph/force_cpp_linker.cpp b/src/_igraph/force_cpp_linker.cpp new file mode 100644 index 000000000..d78b8f3e8 --- /dev/null +++ b/src/_igraph/force_cpp_linker.cpp @@ -0,0 +1,4 @@ +/* The purpose of this file is to make Python use the C++ linker instead of + * the standard C linker because igraph's core static library needs the C++ + * standard library */ + From c1c3d8a819253988c5102fc71511f56e654d044d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 2 Jun 2021 10:26:36 +0200 Subject: [PATCH 0313/1681] ci: run verbose unit tests, add NetworkX in CI env --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2c05589e4..73259dc38 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build and test, upload to PyPI on release on: [push, pull_request] env: - CIBW_TEST_COMMAND: "cd {project} && python -m unittest" + CIBW_TEST_COMMAND: "cd {project} && python -m unittest -vvv" CIBW_SKIP: "cp27-* pp27-* cp35-*" jobs: @@ -128,7 +128,7 @@ jobs: IGRAPH_STATIC_EXTENSION: True IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest" + CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest -vvv" - uses: actions/upload-artifact@v2 with: @@ -170,8 +170,8 @@ jobs: - name: Test run: | - pip install numpy scipy pandas - python -m unittest + pip install numpy scipy pandas networkx + python -m unittest -vvv - uses: actions/upload-artifact@v2 with: From 883a05ebaed0d2c6b8492bd5d7acb4879ba48a59 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 3 Jun 2021 05:39:01 +1000 Subject: [PATCH 0314/1681] Matplotlib plotting and clustering color bugfix (#410) --- src/igraph/drawing/graph.py | 105 +++++++++++++++++++++++------------- 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index c8afe6f14..8c766ff08 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -1024,32 +1024,54 @@ def callback_edge_offset(event): clustering = graph graph = clustering.graph + # Get layout + layout = kwds.get("layout", graph.layout()) + if isinstance(layout, str): + layout = graph.layout(layout) + + # Vertex coordinates + vcoord = layout.coords + # mark groups: the used data structure is eventually the dict option: # tuples of vertex indices as the keys, colors as the values. We # convert other formats into that one here - if "mark_groups" in kwds: - if kwds["mark_groups"] is False: - del kwds["mark_groups"] - elif (kwds["mark_groups"] is True) and (clustering is not None): - pass - elif isinstance(kwds["mark_groups"], (VertexClustering, VertexCover)): - if clustering is not None: - raise ValueError( - "mark_groups cannot override a clustering with another" - ) - else: - clustering = kwds["mark_groups"] - kwds["mark_groups"] = True + if "mark_groups" not in kwds: + kwds["mark_groups"] = False + if kwds["mark_groups"] is False: + pass + elif (kwds["mark_groups"] is True) and (clustering is not None): + pass + elif isinstance(kwds["mark_groups"], (VertexClustering, VertexCover)): + if clustering is not None: + raise ValueError( + "mark_groups cannot override a clustering with another" + ) else: - try: - mg_iter = iter(kwds["mark_groups"]) - except TypeError: - raise TypeError("mark_groups is not in the right format") - kwds["mark_groups"] = dict(mg_iter) + clustering = kwds["mark_groups"] + kwds["mark_groups"] = True + else: + try: + mg_iter = iter(kwds["mark_groups"]) + except TypeError: + raise TypeError("mark_groups is not in the right format") + kwds["mark_groups"] = dict(mg_iter) + + # If a clustering is set and marks are requested but without a specific + # colormap, make the colormap - # If a clustering is set, color vertices and mark groups if requested + # Two things need coloring: vertices and groups/clusters (polygon) + # The coloring needs to be coordinated between the two. if clustering is not None: - if "vertex_color" not in kwds: + # If mark_groups is a dict, we don't need a default color dict, we + # can just use the mark_groups dict. If mark_groups is False and + # vertex_color is set, we don't need either because the colors are + # already fully specified. In all other cases, we need a default + # color dict. + if isinstance(kwds["mark_groups"], dict): + group_colordict = kwds["mark_groups"] + elif (kwds["mark_groups"] is False) and ("vertex_color" in kwds): + pass + else: membership = clustering.membership if isinstance(clustering, VertexCover): membership = [x[0] for x in membership] @@ -1057,26 +1079,35 @@ def callback_edge_offset(event): n_colors = len(clusters) cmap = mpl.cm.get_cmap("viridis") colors = [cmap(1.0 * i / n_colors) for i in range(n_colors)] - c = [colors[clusters.index(i)] for i in membership] - kwds["vertex_color"] = c + cluster_colordict = {g: c for g, c in zip(clusters, colors)} # mark_groups if not explicitly marked - if kwds.get("mark_groups") is True: - mark_groups = defaultdict(list) - for i, color in enumerate(c): - mark_groups[color].append(i) - - # Invert keys and values - mark_groups = {tuple(v): k for k, v in mark_groups.items()} - kwds["mark_groups"] = mark_groups + group_colordict = defaultdict(list) + for i, g in enumerate(membership): + color = cluster_colordict[g] + group_colordict[color].append(i) + del cluster_colordict + # Invert keys and values + group_colordict = {tuple(v): k for k, v in group_colordict.items()} + + # If mark_groups is set but not defined, make a default colormap + if kwds["mark_groups"] is True: + kwds["mark_groups"] = group_colordict - # Get layout - layout = kwds.get("layout", graph.layout()) - if isinstance(layout, str): - layout = graph.layout(layout) - - # Vertex coordinates - vcoord = layout.coords + if "vertex_color" not in kwds: + kwds["vertex_color"] = ['none' for m in membership] + for group_vs, color in group_colordict.items(): + for i in group_vs: + kwds["vertex_color"][i] = color + + # Now mark_groups is either a dict or False + # If vertex_color is not set, we can rely on mark_groups if a dict, + # else we need to make up the same colormap as if we were requested groups + if "vertex_color" not in kwds: + if isinstance(kwds["mark_groups"], dict): + membership = clustering.membership + if isinstance(clustering, VertexCover): + membership = [x[0] for x in membership] # Mark groups if "mark_groups" in kwds: From 0abc0dd50a3d858d0f54ce7883e4c7f661667e45 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 8 Jun 2021 17:56:57 +0200 Subject: [PATCH 0315/1681] fix: set_random_number_generator(None) now properly restores the original igraph RNG, fixes #414 --- src/_igraph/random.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/_igraph/random.c b/src/_igraph/random.c index e267d13d0..ad3f96d22 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -38,6 +38,7 @@ typedef struct { static igraph_i_rng_Python_state_t igraph_rng_Python_state = {0, 0, 0}; static igraph_rng_t igraph_rng_Python = {0, 0, 0}; +static igraph_rng_t igraph_rng_default_saved = {0, 0, 0}; int igraph_rng_Python_init(void **state) { IGRAPH_ERROR("Python RNG error, unsupported function called", @@ -61,7 +62,7 @@ PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) { if (object == Py_None) { /* Reverting to the default igraph random number generator instead * of the Python-based one */ - igraph_rng_set_default(igraph_rng_default()); + igraph_rng_set_default(&igraph_rng_default_saved); Py_RETURN_NONE; } @@ -182,6 +183,10 @@ igraph_rng_type_t igraph_rngtype_Python = { void igraphmodule_init_rng(PyObject* igraph_module) { PyObject* random_module; + if (igraph_rng_default_saved.type == 0) { + igraph_rng_default_saved = *igraph_rng_default(); + } + if (igraph_rng_Python.state != 0) return; @@ -200,5 +205,6 @@ void igraphmodule_init_rng(PyObject* igraph_module) { PyErr_Clear(); return; } + Py_DECREF(random_module); } From fb47e3ec48b15ee751a9a436a2fd300d27c6c271 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 8 Jun 2021 20:55:59 +0200 Subject: [PATCH 0316/1681] fix: PyObject_CallObject() is slightly faster when generating random numbers --- src/_igraph/random.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/random.c b/src/_igraph/random.c index ad3f96d22..235e427e0 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -125,7 +125,7 @@ unsigned long int igraph_rng_Python_get(void *state) { * \brief Generates a real number between 0 and 1 using the Python random number generator. */ igraph_real_t igraph_rng_Python_get_real(void *state) { - PyObject* result = PyObject_CallFunction(igraph_rng_Python_state.random_func, NULL); + PyObject* result = PyObject_CallObject(igraph_rng_Python_state.random_func, 0); double retval; if (result == 0) { From 1b2b0a30dff1fc9f8c00e4cc1dda81543eae7733 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 8 Jun 2021 22:09:38 +0200 Subject: [PATCH 0317/1681] fix: RNG performance improvements --- CHANGELOG.md | 17 +++++ src/_igraph/random.c | 165 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 153 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9aa1aec4..15cb0fe47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # igraph Python interface changelog +## 0.9.5 + +### Fixed + +* `set_random_number_generator(None)` now correctly switches back to igraph's + own random number generator instead of the default one that hooks into + the `random` module of Python. + +* Improved performance in cases when igraph has to call back to Python's + `random` module to generate random numbers. One example is + `Graph.Degree_Sequence(method="vl")`, whose performance suffered a more than + 30x slowdown on 32-bit platforms before, compared to the native C + implementation. Now the gap is smaller. Note that if you need performance and + do not care about seeding the random number generator from Python, you can + now use `set_random_number_generator(None)` to switch back to igraph's own + RNG that does not need a roundtrip to Python. + ## 0.9.4 ### Added diff --git a/src/_igraph/random.c b/src/_igraph/random.c index 235e427e0..0731ec7fa 100644 --- a/src/_igraph/random.c +++ b/src/_igraph/random.c @@ -28,14 +28,29 @@ /** * \ingroup python_interface_rng * \brief Internal data structure for storing references to the - * functions used from Python's random number generator. + * functions and arguments used from Python's random number generator. */ typedef struct { + PyObject* getrandbits_func; PyObject* randint_func; PyObject* random_func; PyObject* gauss_func; + + PyObject* rng_bits_as_pyobject; + PyObject* zero_as_pyobject; + PyObject* one_as_pyobject; + PyObject* rng_max_as_pyobject; } igraph_i_rng_Python_state_t; +/* igraph_rng_get_int31() is potentially faster if the max value of the RNG + * is 0x7FFFFFFF; however, in case of Python, it is actually _slower_ because + * Python long integers are not terribly efficient. We are better off with using + * any other value here */ +#define RNG_MAX 0xFFFFFFFF + +/* This must be consistent with the value of RNG_MAX above */ +#define RNG_BITS 32 + static igraph_i_rng_Python_state_t igraph_rng_Python_state = {0, 0, 0}; static igraph_rng_t igraph_rng_Python = {0, 0, 0}; static igraph_rng_t igraph_rng_default_saved = {0, 0, 0}; @@ -66,25 +81,67 @@ PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) { Py_RETURN_NONE; } -#define GET_FUNC(name) {\ +#define GET_FUNC(name) { \ func = PyObject_GetAttrString(object, name); \ - if (func == 0) \ - return NULL; \ - if (!PyCallable_Check(func)) {\ - PyErr_SetString(PyExc_TypeError, name "attribute must be callable"); \ - return NULL; \ + if (func == 0) {\ + return 0; \ + } else if (!PyCallable_Check(func)) { \ + PyErr_SetString(PyExc_TypeError, "'" name "' attribute must be callable"); \ + return 0; \ } \ } +#define GET_OPTIONAL_FUNC(name) { \ + if (PyObject_HasAttrString(object, name)) { \ + func = PyObject_GetAttrString(object, name); \ + if (func == 0) { \ + return 0; \ + } else if (!PyCallable_Check(func)) { \ + PyErr_SetString(PyExc_TypeError, "'" name "' attribute must be callable"); \ + return 0; \ + } \ + } else { \ + func = 0; \ + } \ +} + + GET_OPTIONAL_FUNC("getrandbits"); new_state.getrandbits_func = func; GET_FUNC("randint"); new_state.randint_func = func; GET_FUNC("random"); new_state.random_func = func; GET_FUNC("gauss"); new_state.gauss_func = func; + /* construct the arguments of getrandbits(RNG_BITS) and randint(0, RNG_MAX) + * in advance */ + new_state.rng_bits_as_pyobject = PyLong_FromLong(RNG_BITS); + if (new_state.rng_bits_as_pyobject == 0) { + return 0; + } + new_state.zero_as_pyobject = PyLong_FromLong(0); + if (new_state.zero_as_pyobject == 0) { + return 0; + } + new_state.one_as_pyobject = PyLong_FromLong(1); + if (new_state.one_as_pyobject == 0) { + return 0; + } + new_state.rng_max_as_pyobject = PyLong_FromUnsignedLong(RNG_MAX); + if (new_state.rng_max_as_pyobject == 0) { + return 0; + } + +#undef GET_FUNC +#undef GET_OPTIONAL_FUNC + old_state = igraph_rng_Python_state; igraph_rng_Python_state = new_state; + Py_XDECREF(old_state.getrandbits_func); Py_XDECREF(old_state.randint_func); Py_XDECREF(old_state.random_func); Py_XDECREF(old_state.gauss_func); + Py_XDECREF(old_state.rng_bits_as_pyobject); + Py_XDECREF(old_state.zero_as_pyobject); + Py_XDECREF(old_state.one_as_pyobject); + Py_XDECREF(old_state.rng_max_as_pyobject); igraph_rng_set_default(&igraph_rng_Python); @@ -106,18 +163,47 @@ int igraph_rng_Python_seed(void *state, unsigned long int seed) { * \brief Generates an unsigned long integer using the Python random number generator. */ unsigned long int igraph_rng_Python_get(void *state) { - PyObject* result = PyObject_CallFunction(igraph_rng_Python_state.randint_func, "kk", 0, LONG_MAX); + PyObject* result; + PyObject* exc_type; unsigned long int retval; + if (igraph_rng_Python_state.getrandbits_func) { + /* This is the preferred code path if the random module given by the user + * supports getrandbits(); it is faster than randint() but still slower + * than simply calling random() */ + result = PyObject_CallFunctionObjArgs( + igraph_rng_Python_state.getrandbits_func, + igraph_rng_Python_state.rng_bits_as_pyobject, + 0 + ); + } else { + /* We want to avoid hitting this path at all costs because randint() is + * very costly in the Python layer */ + result = PyObject_CallFunctionObjArgs( + igraph_rng_Python_state.randint_func, + igraph_rng_Python_state.zero_as_pyobject, + igraph_rng_Python_state.rng_max_as_pyobject, + 0 + ); + } + if (result == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); + exc_type = PyErr_Occurred(); + if (exc_type == PyExc_KeyboardInterrupt) { + /* KeyboardInterrupt is okay, we don't report it, just store it and let + * the caller handler it at the earliest convenience */ + } else { + /* All other exceptions are reported and cleared */ + PyErr_WriteUnraisable(exc_type); + PyErr_Clear(); + } /* Fallback to the C random generator */ - return rand() * LONG_MAX; + return rand() * RNG_MAX; + } else { + retval = PyLong_AsUnsignedLong(result); + Py_DECREF(result); + return retval; } - retval = PyLong_AsLong(result); - Py_DECREF(result); - return retval; } /** @@ -125,19 +211,27 @@ unsigned long int igraph_rng_Python_get(void *state) { * \brief Generates a real number between 0 and 1 using the Python random number generator. */ igraph_real_t igraph_rng_Python_get_real(void *state) { - PyObject* result = PyObject_CallObject(igraph_rng_Python_state.random_func, 0); + PyObject* exc_type; double retval; + PyObject* result = PyObject_CallObject(igraph_rng_Python_state.random_func, 0); if (result == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); + exc_type = PyErr_Occurred(); + if (exc_type == PyExc_KeyboardInterrupt) { + /* KeyboardInterrupt is okay, we don't report it, just store it and let + * the caller handler it at the earliest convenience */ + } else { + /* All other exceptions are reported and cleared */ + PyErr_WriteUnraisable(exc_type); + PyErr_Clear(); + } /* Fallback to the C random generator */ return rand(); + } else { + retval = PyFloat_AsDouble(result); + Py_DECREF(result); + return retval; } - - retval = PyFloat_AsDouble(result); - Py_DECREF(result); - return retval; } /** @@ -146,19 +240,32 @@ igraph_real_t igraph_rng_Python_get_real(void *state) { * around zero with unit variance. */ igraph_real_t igraph_rng_Python_get_norm(void *state) { - PyObject* result = PyObject_CallFunction(igraph_rng_Python_state.gauss_func, "dd", 0.0, 1.0); + PyObject* exc_type; double retval; + PyObject* result = PyObject_CallFunctionObjArgs( + igraph_rng_Python_state.gauss_func, + igraph_rng_Python_state.zero_as_pyobject, + igraph_rng_Python_state.one_as_pyobject, + 0 + ); if (result == 0) { - PyErr_WriteUnraisable(PyErr_Occurred()); - PyErr_Clear(); + exc_type = PyErr_Occurred(); + if (exc_type == PyExc_KeyboardInterrupt) { + /* KeyboardInterrupt is okay, we don't report it, just store it and let + * the caller handler it at the earliest convenience */ + } else { + /* All other exceptions are reported and cleared */ + PyErr_WriteUnraisable(exc_type); + PyErr_Clear(); + } /* Fallback to the C random generator */ return 0; + } else { + retval = PyFloat_AsDouble(result); + Py_DECREF(result); + return retval; } - - retval = PyFloat_AsDouble(result); - Py_DECREF(result); - return retval; } /** @@ -169,7 +276,7 @@ igraph_real_t igraph_rng_Python_get_norm(void *state) { igraph_rng_type_t igraph_rngtype_Python = { /* name= */ "Python random generator", /* min= */ 0, - /* max= */ LONG_MAX, + /* max= */ RNG_MAX, /* init= */ igraph_rng_Python_init, /* destroy= */ igraph_rng_Python_destroy, /* seed= */ igraph_rng_Python_seed, From 0a153b39531c4073b70187167c236a9365a24f87 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 11 Jun 2021 13:01:13 +0200 Subject: [PATCH 0318/1681] chore: bumped version to 0.9.5 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 7bb53614e..1ad803559 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.9.4' +version = '0.9.5' # The full version, including alpha/beta/rc tags. -release = '0.9.4' +release = '0.9.5' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index c7f87fb0d..06e50590c 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 9, 4) +__version_info__ = (0, 9, 5) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 4c19544a873b68c1406e379d9eb6f3371a4edd67 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 11 Jun 2021 17:17:05 +0200 Subject: [PATCH 0319/1681] chore: last tweaks to changelog [ci skip] --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15cb0fe47..e8af890ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Fixed +* `plot(g, ..., mark_groups=True)` now works with the Matplotlib plotting backend. + * `set_random_number_generator(None)` now correctly switches back to igraph's own random number generator instead of the default one that hooks into the `random` module of Python. From 713b811ad472e122ef28047c3eba6f470c8f8c82 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 12 Jun 2021 00:21:11 +0200 Subject: [PATCH 0320/1681] fix: fix failing Matplotlib backend when mark_groups=... is not provided, fixes #415 --- src/igraph/drawing/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 8c766ff08..5b1397013 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -1110,7 +1110,7 @@ def callback_edge_offset(event): membership = [x[0] for x in membership] # Mark groups - if "mark_groups" in kwds: + if "mark_groups" in kwds and isinstance(kwds["mark_groups"], dict): for idx, color in kwds["mark_groups"].items(): points = [vcoord[i] for i in idx] vertices = np.asarray(convex_hull(points, coords=True)) From 03368df2937dcf4871cfd1210eadc21a21c570ae Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 12 Jun 2021 00:23:14 +0200 Subject: [PATCH 0321/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8af890ea..1172c7cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # igraph Python interface changelog +## 0.9.6 + +### Fixed + +* Version 0.9.5 accidentally broke the Matplotlib backend when it was invoked + without the `mark_groups=...` keyword argument; this version fixes the issue. + Thanks to @dschult for reporting it! + ## 0.9.5 ### Fixed From f657a59bc7a7c9b56aefa2b2dff7953d06d27d6f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 12 Jun 2021 10:07:29 +0200 Subject: [PATCH 0322/1681] chore: bumped version to 0.9.56 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 1ad803559..eb5b48839 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.9.5' +version = '0.9.6' # The full version, including alpha/beta/rc tags. -release = '0.9.5' +release = '0.9.6' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index 06e50590c..fd77afc92 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 9, 5) +__version_info__ = (0, 9, 6) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 27b8068faddfb64d6b43b428982803f2f63f48b3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 20 Jun 2021 19:41:35 +0200 Subject: [PATCH 0323/1681] fix: removed deprecated 'grid_fr' layout alias, refs #417 --- doc/source/tutorial.rst | 3 --- src/_igraph/graphobject.h | 1 - src/igraph/__init__.py | 3 --- 3 files changed, 7 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 17a584f49..6d992f67a 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -682,9 +682,6 @@ Method name Short name Algorithm description ``layout_fruchterman_reingold_3d`` ``fr3d``, Fruchterman-Reingold force-directed algorithm ``fr_3d`` in three dimensions ------------------------------------ --------------- --------------------------------------------- -``layout_grid_fruchterman_reingold`` ``grid_fr`` Fruchterman-Reingold force-directed algorithm - with grid heuristics for large graphs ------------------------------------- --------------- --------------------------------------------- ``layout_kamada_kawai`` ``kk`` Kamada-Kawai force-directed algorithm ------------------------------------ --------------- --------------------------------------------- ``layout_kamada_kawai_3d`` ``kk3d``, Kamada-Kawai force-directed algorithm diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index d9d3c1fc8..8ee6e4fab 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -145,7 +145,6 @@ PyObject* igraphmodule_Graph_layout_kamada_kawai_3d(igraphmodule_GraphObject *se PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_layout_fruchterman_reingold(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_layout_fruchterman_reingold_3d(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_grid_fruchterman_reingold(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); PyObject* igraphmodule_Graph_layout_reingold_tilford(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index adf896538..1c1fe4b51 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -4285,11 +4285,8 @@ def intersection(self, other, byname="auto"): "drl": "layout_drl", "fr": "layout_fruchterman_reingold", "fruchterman_reingold": "layout_fruchterman_reingold", - "gfr": "layout_grid_fruchterman_reingold", "graphopt": "layout_graphopt", "grid": "layout_grid", - "grid_fr": "layout_grid_fruchterman_reingold", - "grid_fruchterman_reingold": "layout_grid_fruchterman_reingold", "kk": "layout_kamada_kawai", "kamada_kawai": "layout_kamada_kawai", "lgl": "layout_lgl", From 2981fe193388adfe0ef001e0689858d2cbd2825f Mon Sep 17 00:00:00 2001 From: Fabian Witter Date: Tue, 22 Jun 2021 20:37:45 +0200 Subject: [PATCH 0324/1681] Reimplemented Graph.DataFrame with improved perfomance (#418) * Reimplemented Graph.DataFrame with improved perfomance * Fixed wrong import * Reverted change of default value for param use_vids --- src/igraph/__init__.py | 147 ++++++++++++++++++--------------------- tests/test_generators.py | 55 ++++++++++++++- 2 files changed, 121 insertions(+), 81 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 1c1fe4b51..df4126c1d 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3406,14 +3406,16 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): @param edges: pandas DataFrame containing edges and metadata. The first two columns of this DataFrame contain the source and target vertices - for each edge. These indicate the vertex *names* rather than ids - unless `use_vids` is True and these are nonnegative integers. + for each edge. These indicate the vertex *names* rather than IDs + unless `use_vids` is True and these are non-negative integers. Further + columns may contain edge attributes. @param directed: bool setting whether the graph is directed @param vertices: None (default) or pandas DataFrame containing vertex - metadata. The first column must contain the unique ids of the - vertices and will be set as attribute 'name'. Although vertex names - are usually strings, they can be any hashable object. All other - columns will be added as vertex attributes by column name. + metadata. The first column of the DataFrame must contain the unique + vertex *names*. If `use_vids` is True, the DataFrame's index must + contain the vertex IDs as a sequence of intergers from `0` to + `len(vertices) - 1`. All other columns will be added as vertex + attributes by column name. @use_vids: whether to interpret the first two columns of the `edges` argument as vertex ids (0-based integers) instead of vertex names. If this argument is set to True and the first two columns of `edges` @@ -3426,96 +3428,81 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): to unexpected behaviour: fill your NaNs with values before calling this function to mitigate. """ - import numpy as np - import pandas as pd + try: + import pandas as pd + except ImportError: + raise ImportError("You should install pandas in order to use this function") + try: + import numpy as np + except: + raise ImportError("You should install numpy in order to use this function") if edges.shape[1] < 2: - raise ValueError("the data frame should contain at least two columns") + raise ValueError("The 'edges' DataFrame must contain at least two columns") + if vertices is not None and vertices.shape[1] < 1: + raise ValueError("The 'vertices' DataFrame must contain at least one column") if use_vids: - if str(edges.dtypes[0]).startswith("int") and str( - edges.dtypes[1] - ).startswith("int"): - names_edges = None - else: - raise TypeError("vertex ids must be 0-based integers") - + if not (str(edges.dtypes[0]).startswith("int") and str(edges.dtypes[1]).startswith("int")): + raise TypeError(f"Source and target IDs must be 0-based integers, found types {edges.dtypes.tolist()[:2]}") + elif (edges.iloc[:, :2] < 0).any(axis=None): + raise ValueError("Source and target IDs must not be negative") + if vertices is not None: + vertices = vertices.sort_index() + if not vertices.index.equals(pd.RangeIndex.from_range(range(vertices.shape[0]))): + if not str(vertices.index.dtype).startswith("int"): + raise TypeError(f"Vertex IDs must be 0-based integers, found type {vertices.index.dtype}") + elif (vertices.index < 0).any(axis=None): + raise ValueError("Vertex IDs must not be negative") + else: + raise ValueError(f"Vertex IDs must be an integer sequence from 0 to {vertices.shape[0] - 1}") else: - # Handle if some elements are 'NA' - if edges.iloc[:, :2].isna().values.any(): - warn("In 'edges' NA elements were replaced with string \"NA\"") + # Handle if some source and target names in 'edges' are 'NA' + if edges.iloc[:, :2].isna().any(axis=None): + warn("In the first two columns of 'edges' NA elements were replaced with string \"NA\"") edges = edges.copy() edges.iloc[:, :2].fillna("NA", inplace=True) - names_edges = np.unique(edges.values[:, :2]) - - if (vertices is not None) and vertices.iloc[:, 0].isna().values.any(): - warn( - "In the first column of 'vertices' NA elements were replaced " - + 'with string "NA"' - ) - vertices = vertices.copy() - vertices.iloc[:, 0].fillna("NA", inplace=True) + # Bring DataFrame(s) into same format as with 'use_vids=True' + if vertices is None: + vertices = pd.DataFrame({"name": np.unique(edges.values[:, :2])}) - if vertices is None: - names = names_edges - else: - if vertices.shape[1] < 1: - raise ValueError("vertices has no columns") + if vertices.iloc[:, 0].isna().any(): + warn("In the first column of 'vertices' NA elements were replaced with string \"NA\"") + vertices = vertices.copy() + vertices.iloc[:, 0].fillna("NA", inplace=True) - names_vertices = vertices.iloc[:, 0] - - if names_vertices.duplicated().any(): + if vertices.iloc[:, 0].duplicated().any(): raise ValueError("Vertex names must be unique") - names_vertices = names_vertices.values + if vertices.shape[1] > 1 and "name" in vertices.columns[1:]: + raise ValueError("Vertex attribute conflict: DataFrame already contains column 'name'") - if (names_edges is not None) and len( - np.setdiff1d(names_edges, names_vertices) - ): - raise ValueError( - "Some vertices in the edge DataFrame are missing from " - + "vertices DataFrame" - ) + vertices = vertices.rename({vertices.columns[0]: "name"}, axis=1).reset_index(drop=True) - names = names_vertices + # Map source and target names in 'edges' to IDs + vid_map = pd.Series(vertices.index, index=vertices.iloc[:, 0]) + edges = edges.copy() + edges.iloc[:, 0] = edges.iloc[:, 0].map(vid_map) + edges.iloc[:, 1] = edges.iloc[:, 1].map(vid_map) - # create graph - if names is not None: - nv = len(names) - else: - nv = edges.iloc[:, :2].values.max() + 1 - g = Graph(n=nv, directed=directed) - - # vertex names - if names is not None: - for v, name in zip(g.vs, names): - v["name"] = name - - # vertex attributes - if (vertices is not None) and (vertices.shape[1] > 1): - cols = vertices.columns - for v, (_, attr) in zip(g.vs, vertices.iterrows()): - for an in cols[1:]: - v[an] = attr[an] - - # create edge list - if names is not None: - names_idx = pd.Series(index=names, data=np.arange(len(names))) - e0 = names_idx[edges.values[:, 0]] - e1 = names_idx[edges.values[:, 1]] + # Create graph + if vertices is None: + nv = edges.iloc[:, :2].max().max() + 1 + g = Graph(n=nv, directed=directed) else: - e0 = edges.values[:, 0] - e1 = edges.values[:, 1] - - # add the edges - g.add_edges(list(zip(e0, e1))) - - # edge attributes - if edges.shape[1] > 2: - for e, (_, attr) in zip(g.es, edges.iloc[:, 2:].iterrows()): - for a_name, a_value in list(attr.items()): - e[a_name] = a_value + if not edges.iloc[:, :2].isin(vertices.index).all(axis=None): + raise ValueError("Some vertices in the edge DataFrame are missing from vertices DataFrame") + nv = vertices.shape[0] + g = Graph(n=nv, directed=directed) + # Add vertex attributes + for col in vertices.columns: + g.vs[col] = vertices[col].tolist() + + # add edges including optional attributes + e_list = list(edges.iloc[:, :2].itertuples(index=False, name=None)) + e_attr = edges.iloc[:, 2:].to_dict(orient='list') if edges.shape[1] > 2 else None + g.add_edges(e_list, e_attr) return g diff --git a/tests/test_generators.py b/tests/test_generators.py index 77ce8023b..576ca5b98 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -383,7 +383,7 @@ def testDataFrame(self): g = Graph.DataFrame( edges, directed=True, - vertices=vertices, + vertices=vertices ) self.assertTrue(g.vs["name"] == [1, 2, 3, 4, 5, 6]) self.assertTrue(g.vs["label"] == ["1", "2", "3", "4", "5", "6"]) @@ -397,6 +397,59 @@ def testDataFrame(self): g = Graph.DataFrame(edges, use_vids=True) self.assertTrue(g.vcount() == 7) + # Graph clone + g = Graph.Full(n=100, directed=True, loops=True) + g.vs["name"] = [f"v{i}" for i in range(g.vcount())] + g.vs["x"] = [float(i) for i in range(g.vcount())] + g.es["w"] = [1.0] * g.ecount() + df_edges = g.get_edge_dataframe() + df_vertices = g.get_vertex_dataframe() + g_clone = Graph.DataFrame(df_edges, g.is_directed(), df_vertices, True) + self.assertTrue(df_edges.equals(g_clone.get_edge_dataframe())) + self.assertTrue(df_vertices.equals(g_clone.get_vertex_dataframe())) + + # Invalid input + with self.assertRaisesRegex(ValueError, "two columns"): + edges = pd.DataFrame({"source": [1, 2, 3]}) + Graph.DataFrame(edges) + with self.assertRaisesRegex(ValueError, "one column"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + Graph.DataFrame(edges, vertices=pd.DataFrame()) + with self.assertRaisesRegex(TypeError, "integers"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}).astype(str) + Graph.DataFrame(edges, use_vids=True) + with self.assertRaisesRegex(ValueError, "negative"): + edges = -pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + Graph.DataFrame(edges, use_vids=True) + with self.assertRaisesRegex(TypeError, "integers"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 3]}, index=["1", "2", "3"]) + Graph.DataFrame(edges, vertices=vertices, use_vids=True) + with self.assertRaisesRegex(ValueError, "negative"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 3]}, index=[-1, 2, 3]) + Graph.DataFrame(edges, vertices=vertices, use_vids=True) + with self.assertRaisesRegex(ValueError, "sequence"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 3]}, index=[1, 2, 4]) + Graph.DataFrame(edges, vertices=vertices, use_vids=True) + with self.assertRaisesRegex(TypeError, "integers"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 3]}, index=pd.MultiIndex.from_tuples([(1, 1), (2, 2), (3, 3)])) + Graph.DataFrame(edges, vertices=vertices, use_vids=True) + with self.assertRaisesRegex(ValueError, "unique"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 2]}) + Graph.DataFrame(edges, vertices=vertices) + with self.assertRaisesRegex(ValueError, "already contains"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 3], "name": [1, 2, 2]}) + Graph.DataFrame(edges, vertices=vertices) + with self.assertRaisesRegex(ValueError, "missing from"): + edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) + vertices = pd.DataFrame({0: [1, 2, 3]}, index=[0, 1, 2]) + Graph.DataFrame(edges, vertices=vertices, use_vids=True) + def suite(): generator_suite = unittest.makeSuite(GeneratorTests) From a08df14c00b3ffbe7bddc4d14c891994ad7f02f0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 22 Jun 2021 20:44:22 +0200 Subject: [PATCH 0325/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1172c7cf3..777587a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # igraph Python interface changelog -## 0.9.6 +## [Unreleased] + +### Changed + +* Improved performance of `Graph.DataFrame()`, thanks to + [@fwitter](https://github.com/user/fwitter). See PR + [#418](https://github.com/igraph/python-igraph/pull/418) for more details. + +## [0.9.6] ### Fixed @@ -8,7 +16,7 @@ without the `mark_groups=...` keyword argument; this version fixes the issue. Thanks to @dschult for reporting it! -## 0.9.5 +## [0.9.5] ### Fixed @@ -27,7 +35,7 @@ now use `set_random_number_generator(None)` to switch back to igraph's own RNG that does not need a roundtrip to Python. -## 0.9.4 +## [0.9.4] ### Added @@ -68,7 +76,7 @@ * `igraph.graph.drawing.UbiGraphDrawer` is deprecated as the upstream project is not maintained since 2008. -## 0.9.1 +## [0.9.1] ### Changed @@ -101,7 +109,7 @@ official Python wheels. -## 0.9.0 +## [0.9.0] ### Added @@ -158,9 +166,17 @@ Python interface. -## 0.8.3 +## [0.8.3] This is the last released version of `python-igraph` without a changelog file. Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. + +[Unreleased]: https://github.com/igraph/igraph/compare/0.9.6..HEAD +[0.9.6]: https://github.com/igraph/igraph/compare/0.9.5...0.9.6 +[0.9.5]: https://github.com/igraph/igraph/compare/0.9.4...0.9.5 +[0.9.4]: https://github.com/igraph/igraph/compare/0.9.1...0.9.4 +[0.9.1]: https://github.com/igraph/igraph/compare/0.9.0...0.9.1 +[0.9.0]: https://github.com/igraph/igraph/compare/0.8.5...0.9.0 +[0.8.3]: https://github.com/igraph/igraph/releases/tag/0.8.3 From 8e3f00beb42d6a3bdc55a8cccf7dc7bba8e8f5c9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 22 Jun 2021 20:45:11 +0200 Subject: [PATCH 0326/1681] fix: fix links in changelog [ci skip] --- CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 777587a26..e6d9c4755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -173,10 +173,10 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[Unreleased]: https://github.com/igraph/igraph/compare/0.9.6..HEAD -[0.9.6]: https://github.com/igraph/igraph/compare/0.9.5...0.9.6 -[0.9.5]: https://github.com/igraph/igraph/compare/0.9.4...0.9.5 -[0.9.4]: https://github.com/igraph/igraph/compare/0.9.1...0.9.4 -[0.9.1]: https://github.com/igraph/igraph/compare/0.9.0...0.9.1 -[0.9.0]: https://github.com/igraph/igraph/compare/0.8.5...0.9.0 -[0.8.3]: https://github.com/igraph/igraph/releases/tag/0.8.3 +[Unreleased]: https://github.com/igraph/python-igraph/compare/0.9.6..HEAD +[0.9.6]: https://github.com/igraph/python-igraph/compare/0.9.5...0.9.6 +[0.9.5]: https://github.com/igraph/python-igraph/compare/0.9.4...0.9.5 +[0.9.4]: https://github.com/igraph/python-igraph/compare/0.9.1...0.9.4 +[0.9.1]: https://github.com/igraph/python-igraph/compare/0.9.0...0.9.1 +[0.9.0]: https://github.com/igraph/python-igraph/compare/0.8.5...0.9.0 +[0.8.3]: https://github.com/igraph/python-igraph/releases/tag/0.8.3 From 1e7d1dd9d674fdcc3115f245beb469bd48982496 Mon Sep 17 00:00:00 2001 From: Fabian Witter Date: Wed, 23 Jun 2021 20:16:24 +0200 Subject: [PATCH 0327/1681] Changed default value of param use_vids in Graph.DataFrame to False --- src/igraph/__init__.py | 19 ++++++++++--------- tests/test_generators.py | 31 ++++++++++++++++--------------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index df4126c1d..7caf06f1f 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -3401,21 +3401,22 @@ def Incidence( return result @classmethod - def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): + def DataFrame(cls, edges, directed=True, vertices=None, use_vids=True): """Generates a graph from one or two dataframes. @param edges: pandas DataFrame containing edges and metadata. The first two columns of this DataFrame contain the source and target vertices - for each edge. These indicate the vertex *names* rather than IDs - unless `use_vids` is True and these are non-negative integers. Further - columns may contain edge attributes. + for each edge. These indicate the vertex IDs as nonnegative integers + rather than vertex *names* unless `use_vids` is False. Further columns + may contain edge attributes. @param directed: bool setting whether the graph is directed @param vertices: None (default) or pandas DataFrame containing vertex - metadata. The first column of the DataFrame must contain the unique - vertex *names*. If `use_vids` is True, the DataFrame's index must - contain the vertex IDs as a sequence of intergers from `0` to - `len(vertices) - 1`. All other columns will be added as vertex - attributes by column name. + metadata. The DataFrame's index must contain the vertex IDs as a + sequence of intergers from `0` to `len(vertices) - 1`. If `use_vids` + is False, the first column must contain the unique vertex *names*. + Although vertex names are usually strings, they can be any hashable + object. All other columns will be added as vertex attributes by column + name. @use_vids: whether to interpret the first two columns of the `edges` argument as vertex ids (0-based integers) instead of vertex names. If this argument is set to True and the first two columns of `edges` diff --git a/tests/test_generators.py b/tests/test_generators.py index 576ca5b98..a492511c0 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -364,13 +364,13 @@ def testDataFrame(self): edges = pd.DataFrame( [["C", "A", 0.4], ["A", "B", 0.1]], columns=[0, 1, "weight"] ) - g = Graph.DataFrame(edges, directed=False) + g = Graph.DataFrame(edges, directed=False, use_vids=False) self.assertTrue(g.es["weight"] == [0.4, 0.1]) vertices = pd.DataFrame( [["A", "blue"], ["B", "yellow"], ["C", "blue"]], columns=[0, "color"] ) - g = Graph.DataFrame(edges, directed=True, vertices=vertices) + g = Graph.DataFrame(edges, directed=True, vertices=vertices, use_vids=False) self.assertTrue(g.vs["name"] == ["A", "B", "C"]) self.assertTrue(g.vs["color"] == ["blue", "yellow", "blue"]) self.assertTrue(g.es["weight"] == [0.4, 0.1]) @@ -383,18 +383,19 @@ def testDataFrame(self): g = Graph.DataFrame( edges, directed=True, - vertices=vertices + vertices=vertices, + use_vids=False ) self.assertTrue(g.vs["name"] == [1, 2, 3, 4, 5, 6]) self.assertTrue(g.vs["label"] == ["1", "2", "3", "4", "5", "6"]) # Vertex ids edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) - g = Graph.DataFrame(edges) + g = Graph.DataFrame(edges, use_vids=False) self.assertTrue(g.vcount() == 6) edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) - g = Graph.DataFrame(edges, use_vids=True) + g = Graph.DataFrame(edges) self.assertTrue(g.vcount() == 7) # Graph clone @@ -404,7 +405,7 @@ def testDataFrame(self): g.es["w"] = [1.0] * g.ecount() df_edges = g.get_edge_dataframe() df_vertices = g.get_vertex_dataframe() - g_clone = Graph.DataFrame(df_edges, g.is_directed(), df_vertices, True) + g_clone = Graph.DataFrame(df_edges, g.is_directed(), df_vertices) self.assertTrue(df_edges.equals(g_clone.get_edge_dataframe())) self.assertTrue(df_vertices.equals(g_clone.get_vertex_dataframe())) @@ -417,38 +418,38 @@ def testDataFrame(self): Graph.DataFrame(edges, vertices=pd.DataFrame()) with self.assertRaisesRegex(TypeError, "integers"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}).astype(str) - Graph.DataFrame(edges, use_vids=True) + Graph.DataFrame(edges) with self.assertRaisesRegex(ValueError, "negative"): edges = -pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) - Graph.DataFrame(edges, use_vids=True) + Graph.DataFrame(edges) with self.assertRaisesRegex(TypeError, "integers"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 3]}, index=["1", "2", "3"]) - Graph.DataFrame(edges, vertices=vertices, use_vids=True) + Graph.DataFrame(edges, vertices=vertices) with self.assertRaisesRegex(ValueError, "negative"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 3]}, index=[-1, 2, 3]) - Graph.DataFrame(edges, vertices=vertices, use_vids=True) + Graph.DataFrame(edges, vertices=vertices) with self.assertRaisesRegex(ValueError, "sequence"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 3]}, index=[1, 2, 4]) - Graph.DataFrame(edges, vertices=vertices, use_vids=True) + Graph.DataFrame(edges, vertices=vertices) with self.assertRaisesRegex(TypeError, "integers"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 3]}, index=pd.MultiIndex.from_tuples([(1, 1), (2, 2), (3, 3)])) - Graph.DataFrame(edges, vertices=vertices, use_vids=True) + Graph.DataFrame(edges, vertices=vertices) with self.assertRaisesRegex(ValueError, "unique"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 2]}) - Graph.DataFrame(edges, vertices=vertices) + Graph.DataFrame(edges, vertices=vertices, use_vids=False) with self.assertRaisesRegex(ValueError, "already contains"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 3], "name": [1, 2, 2]}) - Graph.DataFrame(edges, vertices=vertices) + Graph.DataFrame(edges, vertices=vertices, use_vids=False) with self.assertRaisesRegex(ValueError, "missing from"): edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) vertices = pd.DataFrame({0: [1, 2, 3]}, index=[0, 1, 2]) - Graph.DataFrame(edges, vertices=vertices, use_vids=True) + Graph.DataFrame(edges, vertices=vertices) def suite(): From f0228316d2f3ec517bfef0e2aee43c96d3fa7cf0 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 29 Jun 2021 18:14:45 +1000 Subject: [PATCH 0328/1681] Bugfix for pop --- src/igraph/operators.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/igraph/operators.py b/src/igraph/operators.py index eaf5b0075..94c47fce4 100644 --- a/src/igraph/operators.py +++ b/src/igraph/operators.py @@ -59,7 +59,8 @@ def disjoint_union(graphs): # New conflict a_conflict.add(a_name) igf = a_first_graph[a_name] - graph_union["{:}_{:}".format(a_name, igf)] = graph_union.pop(a_name) + graph_union["{:}_{:}".format(a_name, igf)] = graph_union[a_name] + del graph_union[a_name] graph_union["{:}_{:}".format(a_name, ig)] = a_value # Vertex attributes @@ -182,7 +183,10 @@ def union(graphs, byname="auto"): # New conflict a_conflict.add(a_name) igf = a_first_graph[a_name] - graph_union["{:}_{:}".format(a_name, igf)] = graph_union.pop(a_name) + # Delete the previous attribute and set attribute with + # a record about the graph of origin + graph_union["{:}_{:}".format(a_name, igf)] = graph_union[a_name] + del graph_union[a_name] graph_union["{:}_{:}".format(a_name, ig)] = a_value # Vertex attributes @@ -371,7 +375,8 @@ def intersection(graphs, byname="auto", keep_all_vertices=True): # New conflict a_conflict.add(a_name) igf = a_first_graph[a_name] - graph_intsec["{:}_{:}".format(a_name, igf)] = graph_intsec.pop(a_name) + graph_intsec["{:}_{:}".format(a_name, igf)] = graph_intsec[a_name] + del graph_intsec[a_name] graph_intsec["{:}_{:}".format(a_name, ig)] = a_value # Vertex attributes From 9416c1f9a92ba48c53f7fd3c97738edb2699f2fc Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 29 Jun 2021 18:21:04 +1000 Subject: [PATCH 0329/1681] Test function --- tests/test_operators.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_operators.py b/tests/test_operators.py index 04a0fe08e..fc3616624 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -128,6 +128,34 @@ def testUnion(self): ] ) + def testUnionWithConflict(self): + g1 = Graph.Tree(7, 2) + g1['name'] = 'Tree' + g2 = Graph.Lattice([7]) + g2['name'] = 'Lattice' + g = union([g1, g2]) # Issue 422 + self.assertTrue( + sorted(g.get_edgelist()) + == [ + (0, 1), + (0, 2), + (0, 6), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 5), + (2, 6), + (3, 4), + (4, 5), + (5, 6), + ] + ) + self.assertTrue( + sorted(g.attributes()), + ['name_1', 'name_2'], + ) + def testUnionMethod(self): g = Graph.Tree(7, 2).union(Graph.Lattice([7])) self.assertTrue(g.vcount() == 7 and g.ecount() == 12) From 329b75795aa138134ee889aa5e7616eff4508b2e Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 29 Jun 2021 19:25:35 +1000 Subject: [PATCH 0330/1681] Improve matplotlib tutorial (#424) --- doc/source/visualisation.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index 7f12491d2..a67fc90be 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -123,6 +123,27 @@ You can then further manipulate the axes and figure however you like via the `ax called them). This variant does not use `Cairo`_ directly and might be lacking some features that are available in the `Cairo`_ backend: please open an issue on Github to request specific features. +Plotting via `matplotlib`_ makes it easy to combine igraph with other plots. For instance, if you want to have a figure +with two panels showing different aspects of some data set, say a graph and a bar plot, you can easily do that: + +>>> import matplotlib.pyplot as plt +>>> fig, axs = plt.subplots(1, 2, figsize=(8, 4)) +>>> ig.plot(g, target=axs[0]) +>>> axs[1].bar(x=[0, 1, 2], height=[1, 5, 3], color='tomato') + +Another common situation is modifying the graph plot after the fact, to achieve some kind of customization. For instance, +you might want to change the size and color of the vertices: + +>>> import matplotlib.pyplot as plt +>>> fig, ax = plt.subplots() +>>> ig.plot(g, target=ax) +>>> dots = ax.get_children()[0] # This is a PathCollection +>>> dots.set_color('tomato') +>>> dots.set_sizes([250] * g.vcount()) + +That also helps as a workaround if you cannot figure out how to use the plotting options below: just use the defaults and +then customize the appearance of your graph via standard `matplotlib`_ tools. + Plotting graphs in Jupyter notebooks ++++++++++++++++++++++++++++++++++++ |igraph| supports inline plots within a `Jupyter`_ notebook via both the `Cairo`_ and `matplotlib`_ backend. If you are From 6fbe4f77bbbc19d8a9df74c1147156931b321399 Mon Sep 17 00:00:00 2001 From: szcf-weiya Date: Mon, 12 Jul 2021 17:39:56 +0800 Subject: [PATCH 0331/1681] add doc for font-family (#427) It seems no documentation on the customization of font-family, although this feature has been implemented in https://github.com/igraph/python-igraph/pull/32/commits Without such documentation, until I came across an old question posted in https://stackoverflow.com/questions/24231430/how-to-use-custom-typeface-for-vertex-labels-in-python-igraph, and then checked the related source code, I got how to specify the font family. --- doc/source/tutorial.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 6d992f67a..54cef9b68 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -824,6 +824,8 @@ Attribute name Keyword argument Purpose =============== ====================== ========================================== ``color`` ``vertex_color`` Color of the vertex --------------- ---------------------- ------------------------------------------ +``font`` ``vertex_font`` Font family of the vertex +--------------- ---------------------- ------------------------------------------ ``label`` ``vertex_label`` Label of the vertex --------------- ---------------------- ------------------------------------------ ``label_angle`` ``vertex_label_angle`` The placement of the vertex label on the @@ -871,6 +873,8 @@ Attribute name Keyword argument Purpose ``autocurve`` keyword argument to :func:`plot`. --------------- ---------------------- ------------------------------------------ +``font`` ``edge_font`` Font family of the edge +--------------- ---------------------- ------------------------------------------ ``arrow_size`` ``edge_arrow_size`` Size (length) of the arrowhead on the edge if the graph is directed, relative to 15 pixels. From 372b86c54a369343b84b2c4b0b67f762ecb2ede7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sun, 8 Aug 2021 12:08:15 +1000 Subject: [PATCH 0332/1681] First int64 step: delete functions, adapt some signatures --- src/_igraph/convert.c | 212 +------------------------------------- src/_igraph/convert.h | 4 - src/_igraph/graphobject.c | 64 +++++++----- 3 files changed, 41 insertions(+), 239 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index f45cc4d3e..582800d38 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1165,116 +1165,6 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v return 0; } -/** - * \ingroup python_interface_conversion - * \brief Converts a Python list of ints to an igraph \c igraph_vector_long_t - * The incoming \c igraph_vector_long_t should be uninitialized. - * Raises suitable Python exceptions when needed. - * - * This function is almost identical to - * \ref igraphmodule_PyObject_to_vector_t . Make sure you fix bugs - * in both cases (if any). - * - * \param list the Python list to be converted - * \param v the \c igraph_vector_long_t containing the result - * \return 0 if everything was OK, 1 otherwise - */ -int igraphmodule_PyObject_to_vector_long_t(PyObject *list, igraph_vector_long_t *v) { - PyObject *item; - long value=0; - Py_ssize_t i, j, k; - int ok; - - if (PyBaseString_Check(list)) { - /* It is highly unlikely that a string (although it is a sequence) will - * provide us with integers or integer pairs */ - PyErr_SetString(PyExc_TypeError, "expected a sequence or an iterable containing integers"); - return 1; - } - - if (!PySequence_Check(list)) { - /* try to use an iterator */ - PyObject *it = PyObject_GetIter(list); - if (it) { - PyObject *item; - igraph_vector_long_init(v, 0); - while ((item = PyIter_Next(it)) != 0) { - ok = 1; - if (!PyNumber_Check(item)) { - PyErr_SetString(PyExc_TypeError, "iterable must return numbers"); - ok=0; - } else { - PyObject *item2 = PyNumber_Long(item); - if (item2 == 0) { - PyErr_SetString(PyExc_TypeError, "can't convert a list item to integer"); - ok = 0; - } else { - value=(long)PyLong_AsLong(item); - Py_DECREF(item2); - } - } - - if (ok == 0) { - igraph_vector_long_destroy(v); - Py_DECREF(item); - Py_DECREF(it); - return 1; - } - if (igraph_vector_long_push_back(v, value)) { - igraphmodule_handle_igraph_error(); - igraph_vector_long_destroy(v); - Py_DECREF(item); - Py_DECREF(it); - return 1; - } - Py_DECREF(item); - } - Py_DECREF(it); - return 0; - } else { - PyErr_SetString(PyExc_TypeError, "sequence or iterable expected"); - return 1; - } - return 0; - } - - j=PySequence_Size(list); - igraph_vector_long_init(v, j); - for (i=0, k=0; ig, types, matching, &result)) { - if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } + if (matching != 0) { igraph_vector_int_destroy(matching); free(matching); } if (types != 0) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; } - if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } + if (matching != 0) { igraph_vector_int_destroy(matching); free(matching); } if (types != 0) { igraph_vector_bool_destroy(types); free(types); } if (result) @@ -485,7 +485,7 @@ PyObject *igraphmodule_Graph_is_maximal_matching(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds) { static char* kwlist[] = { "matching", "types", NULL }; PyObject *matching_o, *types_o = Py_None; - igraph_vector_long_t* matching = 0; + igraph_vector_int_t* matching = 0; igraph_vector_bool_t* types = 0; igraph_bool_t result; @@ -493,23 +493,23 @@ PyObject *igraphmodule_Graph_is_maximal_matching(igraphmodule_GraphObject* self, &types_o)) return NULL; - if (igraphmodule_attrib_to_vector_long_t(matching_o, self, &matching, + if (igraphmodule_attrib_to_vector_int_t(matching_o, self, &matching, ATTRIBUTE_TYPE_VERTEX)) return NULL; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) { - if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } + if (matching != 0) { igraph_vector_int_destroy(matching); free(matching); } return NULL; } if (igraph_is_maximal_matching(&self->g, types, matching, &result)) { - if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } + if (matching != 0) { igraph_vector_int_destroy(matching); free(matching); } if (types != 0) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; } - if (matching != 0) { igraph_vector_long_destroy(matching); free(matching); } + if (matching != 0) { igraph_vector_int_destroy(matching); free(matching); } if (types != 0) { igraph_vector_bool_destroy(types); free(types); } if (result) @@ -1586,7 +1586,9 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, ATTRIBUTE_TYPE_EDGE)) return NULL; if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &diameter, 0, 0, 0, + if (igraph_diameter_dijkstra(&self->g, weights, &diameter, + /* from, to, vertex_path, edge_path */ + 0, 0, 0, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(weights); free(weights); @@ -1595,8 +1597,10 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, igraph_vector_destroy(weights); free(weights); return PyFloat_FromDouble((double)diameter); } else { - if (igraph_diameter(&self->g, &diameter, 0, 0, 0, PyObject_IsTrue(dir), - PyObject_IsTrue(vcount_if_unconnected))) { + if (igraph_diameter(&self->g, &diameter, + /* from, to, vertex_path, edge_path */ + 0, 0, 0, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -1635,7 +1639,9 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, igraph_vector_init(&res, 0); if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, 0, 0, 0, &res, + if (igraph_diameter_dijkstra(&self->g, weights, 0, + /* from, to, vertex_path, edge_path */ + 0, 0, &res, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(weights); free(weights); @@ -1644,8 +1650,10 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, } igraph_vector_destroy(weights); free(weights); } else { - if (igraph_diameter(&self->g, 0, 0, 0, &res, PyObject_IsTrue(dir), - PyObject_IsTrue(vcount_if_unconnected))) { + if (igraph_diameter(&self->g, 0, + /* from, to, vertex_path, edge_path */ + 0, 0, &res, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -1680,7 +1688,9 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, ATTRIBUTE_TYPE_EDGE)) return NULL; if (weights) { - if (igraph_diameter_dijkstra(&self->g, weights, &len, &from, &to, 0, + if (igraph_diameter_dijkstra(&self->g, weights, &len, + /* from, to, vertex_path, edge_path */ + &from, &to, 0, 0, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(weights); free(weights); @@ -1693,8 +1703,10 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, return Py_BuildValue("OOd", Py_None, Py_None, (double)len); } } else { - if (igraph_diameter(&self->g, &len, &from, &to, 0, PyObject_IsTrue(dir), - PyObject_IsTrue(vcount_if_unconnected))) { + if (igraph_diameter(&self->g, &len, + /* from, to, vertex_path, edge_path */ + &from, &to, 0, 0, + PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -5039,7 +5051,9 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * return NULL; } - if (igraph_get_all_shortest_paths_dijkstra(&self->g, &res, + if (igraph_get_all_shortest_paths_dijkstra(&self->g, + /* vertices, edges */ + &res, NULL, NULL, from, to, weights, mode)) { igraphmodule_handle_igraph_error(); igraph_vector_ptr_destroy(&res); @@ -10967,7 +10981,7 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject PyObject *types_o = Py_None, *weights_o = Py_None, *result_o; igraph_vector_bool_t* types = 0; igraph_vector_t* weights = 0; - igraph_vector_long_t result; + igraph_vector_int_t result; double eps = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|Od", kwlist, &types_o, @@ -10985,7 +10999,7 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject return NULL; } - if (igraph_vector_long_init(&result, 0)) { + if (igraph_vector_int_init(&result, 0)) { if (types != 0) { igraph_vector_bool_destroy(types); free(types); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } igraphmodule_handle_igraph_error(); @@ -10995,7 +11009,7 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject if (igraph_maximum_bipartite_matching(&self->g, types, 0, 0, &result, weights, eps)) { if (types != 0) { igraph_vector_bool_destroy(types); free(types); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - igraph_vector_long_destroy(&result); + igraph_vector_int_destroy(&result); igraphmodule_handle_igraph_error(); return NULL; } @@ -11003,8 +11017,8 @@ PyObject *igraphmodule_Graph_maximum_bipartite_matching(igraphmodule_GraphObject if (types != 0) { igraph_vector_bool_destroy(types); free(types); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - result_o = igraphmodule_vector_long_t_to_PyList(&result); - igraph_vector_long_destroy(&result); + result_o = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return result_o; } From bc0cc6bebf89fe2c45f21d09c150e303007799ca Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 9 Aug 2021 13:10:11 +0200 Subject: [PATCH 0333/1681] chore: removed old references and workarounds for Python 2.x --- doc/source/install.rst | 4 +--- doc/source/tutorial.rst | 6 +++--- src/_igraph/common.h | 25 ------------------------- src/_igraph/convert.c | 5 +---- src/_igraph/edgeseqobject.c | 5 +---- src/_igraph/vertexseqobject.c | 6 +----- src/igraph/drawing/__init__.py | 7 +------ 7 files changed, 8 insertions(+), 50 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 22b16492f..45c3759fc 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -36,9 +36,7 @@ Installing |igraph| from the Python Package Index To ensure getting the latest binary release of |igraph|'s Python interface, it is recommended that you install it from the `Python Package Index `_ (PyPI), which has installers for Windows, Linux, -and macOS. Currently there are binary packages for Python 2.7, and Python 3.5 through 3.8, but -note that support for Python 2.7 will be discontinued with the version 0.9.0 release of -|igraph|'s Python interface. +and macOS. We aim to provide binary packages for the three latest minor versions of Python 3.x. To install |python-igraph| globally, use the following command (you probably need administrator/root priviledges): diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 54cef9b68..b9d7cf25e 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -24,8 +24,8 @@ Starting |igraph| ordinary Python module at the Python prompt:: $ python - Python 2.7.1 (r271:86832, Jun 16 2011, 16:59:05) - [GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)] on darwin + Python 3.9.6 (default, Jun 29 2021, 05:25:02) + [Clang 12.0.5 (clang-1205.0.22.9)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> import igraph @@ -57,7 +57,7 @@ When you start the script, you will see something like this:: $ igraph No configuration file, using defaults - igraph 0.6 running inside Python 2.7.1 (r271:86832, Jun 16 2011, 16:59:05) + igraph 0.9.4 running inside Python 3.9.6 (default, Jun 29 2021, 05:25:02) Type "copyright", "credits" or "license" for more information. >>> diff --git a/src/_igraph/common.h b/src/_igraph/common.h index ca301faf0..e74a6fa67 100644 --- a/src/_igraph/common.h +++ b/src/_igraph/common.h @@ -41,35 +41,10 @@ # define RC_TRAVERSE(T, P) #endif -/* Compatibility stuff for Python 2.3 */ -#ifndef Py_RETURN_TRUE -#define Py_RETURN_TRUE { Py_INCREF(Py_True); return Py_True; } -#endif - -#ifndef Py_RETURN_FALSE -#define Py_RETURN_FALSE { Py_INCREF(Py_False); return Py_False; } -#endif - -#ifndef Py_RETURN_NONE -#define Py_RETURN_NONE { Py_INCREF(Py_None); return Py_None; } -#endif - -#ifndef Py_RETURN_NOTIMPLEMENTED -#define Py_RETURN_NOTIMPLEMENTED { Py_INCREF(Py_NotImplemented); return Py_NotImplemented; } -#endif - #ifndef Py_RETURN #define Py_RETURN(x) { if (x) { Py_RETURN_TRUE; } else { Py_RETURN_FALSE; } } #endif -/* Compatibility stuff for Python 2.4 */ -#if (PY_MAJOR_VERSION <= 2) & (PY_MINOR_VERSION <= 4) -#define lenfunc inquiry -#define ssizeargfunc intargfunc -#define ssizessizeargfunc intintargfunc -#define Py_ssize_t int -#endif - #define ATTRIBUTE_TYPE_VERTEX 1 #define ATTRIBUTE_TYPE_EDGE 2 diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index f45cc4d3e..717158136 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2565,10 +2565,7 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, Py_ssize_t no_of_vertices = igraph_vcount(graph); Py_ssize_t start, stop, step, slicelength, i; - /* Casting to void* because Python 2.x expects PySliceObject* - * but Python 3.x expects PyObject* */ - if (PySlice_GetIndicesEx((void*)o, no_of_vertices, - &start, &stop, &step, &slicelength)) + if (PySlice_GetIndicesEx(o, no_of_vertices, &start, &stop, &step, &slicelength)) return 1; if (start == 0 && slicelength == no_of_vertices) { diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index dc7457784..c2cef6a0f 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -708,10 +708,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject PyObject* range; igraph_bool_t ok; - /* Casting to void* because Python 2.x expects PySliceObject* - * but Python 3.x expects PyObject* */ - ok = (PySlice_GetIndicesEx((void*)item, igraph_vector_size(&v2), - &start, &stop, &step, &sl) == 0); + ok = (PySlice_GetIndicesEx(item, igraph_vector_size(&v2), &start, &stop, &step, &sl) == 0); if (ok) { range = igraphmodule_PyRange_create(start, stop, step); ok = (range != 0); diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 77be5ce69..17c157945 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -740,11 +740,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject* range; igraph_bool_t ok; - /* Casting to void* because Python 2.x expects PySliceObject* - * but Python 3.x expects PyObject* */ - ok = (PySlice_GetIndicesEx((void*)item, igraph_vector_size(&v2), - &start, &stop, &step, &sl) == 0); - + ok = (PySlice_GetIndicesEx(item, igraph_vector_size(&v2), &start, &stop, &step, &sl) == 0); if (ok) { range = igraphmodule_PyRange_create(start, stop, step); ok = (range != 0); diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index a59bb7f86..84fd3e4a7 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -369,12 +369,7 @@ def _repr_svg_(self): context.show_page() surface.finish() # Return the raw SVG representation - result = io.getvalue() - if hasattr(result, "encode"): - result = result.encode("utf-8") # for Python 2.x - else: - result = result.decode("utf-8") # for Python 3.x - + result = io.getvalue().decode("utf-8") return result, {"isolated": True} # put it inside an iframe @property From aa09f40ed860eebc787750f52e8bcb137e43c093 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 9 Aug 2021 13:46:41 +0200 Subject: [PATCH 0334/1681] fix: remove all old Python 2.x-style classes and super() calls --- setup.py | 4 ++-- src/igraph/__init__.py | 8 ++++---- src/igraph/app/shell.py | 4 ++-- src/igraph/clustering.py | 6 +++--- src/igraph/configuration.py | 4 ++-- src/igraph/cut.py | 2 +- src/igraph/datatypes.py | 4 ++-- src/igraph/drawing/__init__.py | 2 +- src/igraph/drawing/baseclasses.py | 2 +- src/igraph/drawing/colors.py | 2 +- src/igraph/drawing/edge.py | 8 ++++---- src/igraph/drawing/graph.py | 6 +++--- src/igraph/drawing/metamagic.py | 6 +++--- src/igraph/drawing/shapes.py | 6 +++--- src/igraph/drawing/text.py | 4 ++-- src/igraph/drawing/utils.py | 2 +- src/igraph/layout.py | 2 +- src/igraph/matching.py | 2 +- src/igraph/remote/gephi.py | 6 +++--- src/igraph/statistics.py | 6 +++--- src/igraph/summary.py | 4 ++-- tests/test_atlas.py | 2 +- tests/test_basic.py | 4 ++-- tests/test_cliques.py | 2 +- tests/test_rng.py | 4 ++-- 25 files changed, 51 insertions(+), 51 deletions(-) diff --git a/setup.py b/setup.py index 9163cabd2..0b64a20f2 100644 --- a/setup.py +++ b/setup.py @@ -177,7 +177,7 @@ def wait_for_keypress(seconds): ########################################################################### -class IgraphCCoreBuilder(object): +class IgraphCCoreBuilder: """Superclass for classes responsible for downloading and building the C core of igraph if it is not installed yet. """ @@ -290,7 +290,7 @@ def compile_in(self, source_folder, build_folder, install_folder): ########################################################################### -class BuildConfiguration(object): +class BuildConfiguration: def __init__(self): self.include_dirs = [] self.library_dirs = [] diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index df4126c1d..530cbc3cb 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2294,7 +2294,7 @@ def Read_DIMACS(cls, f, directed=False): vertices are attached as graph attributes C{source} and C{target}, the edge capacities are stored in the C{capacity} edge attribute. """ - graph, source, target, cap = super(Graph, cls).Read_DIMACS(f, directed) + graph, source, target, cap = super().Read_DIMACS(f, directed) graph.es["capacity"] = cap graph["source"] = source graph["target"] = target @@ -3619,7 +3619,7 @@ def bipartite_projection( is not 1 or 2, or the projected one-mode graph specified by the C{which} argument if its value is 0, 1, C{False} or C{True}. """ - superclass_meth = super(Graph, self).bipartite_projection + superclass_meth = super().bipartite_projection if which is False: which = 0 @@ -3665,7 +3665,7 @@ def bipartite_projection_size(self, types="type", *args, **kwds): first projection, followed by the number of vertices and edges in the second projection. """ - return super(Graph, self).bipartite_projection_size(types, *args, **kwds) + return super().bipartite_projection_size(types, *args, **kwds) def get_incidence(self, types="type", *args, **kwds): """Returns the incidence matrix of a bipartite graph. The incidence matrix @@ -3680,7 +3680,7 @@ def get_incidence(self, types="type", *args, **kwds): original vertex IDs. The second list is the same for the column indices. """ - return super(Graph, self).get_incidence(types, *args, **kwds) + return super().get_incidence(types, *args, **kwds) ########################### # DFS (C version will come soon) diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index e553cf69d..cb450c32a 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -285,7 +285,7 @@ def clear(self): self.last_message = "" -class Shell(object): +class Shell: """Superclass of the embeddable shells supported by igraph""" def __init__(self): @@ -362,7 +362,7 @@ def __call__(self): self._root.destroy() -class ConsoleProgressBarMixin(object): +class ConsoleProgressBarMixin: """Mixin class for console shells that support a progress bar.""" def __init__(self): diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 665fc9f3a..67ef19bb7 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -15,7 +15,7 @@ from igraph.utils import str_to_orientation -class Clustering(object): +class Clustering: """Class representing a clustering of an arbitrary ordered set. This is now used as a base for L{VertexClustering}, but it might be @@ -514,7 +514,7 @@ def _formatted_cluster_iterator(self): ############################################################################### -class Dendrogram(object): +class Dendrogram: """The hierarchical clustering (dendrogram) of some dataset. A hierarchical clustering means that we know not only the way the @@ -1034,7 +1034,7 @@ class VisualVertexBuilder(AttributeCollectorBase): ############################################################################### -class Cover(object): +class Cover: """Class representing a cover of an arbitrary ordered set. Covers are similar to clusterings, but each element of the set may diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index edfb00815..43d890219 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -85,7 +85,7 @@ def get_platform_image_viewer(): return "" -class Configuration(object): +class Configuration: """Class representing igraph configuration details. General ideas @@ -183,7 +183,7 @@ class Configuration(object): inline in IPython's console if the console supports it. Default: C{True} """ - class Types(object): + class Types: """Static class for the implementation of custom getter/setter functions for configuration keys""" diff --git a/src/igraph/cut.py b/src/igraph/cut.py index 0f49193bb..c6be762d4 100644 --- a/src/igraph/cut.py +++ b/src/igraph/cut.py @@ -154,7 +154,7 @@ def __init__(self, graph, value, flow, cut, partition): This should not be called directly, everything is taken care of by L{Graph.maxflow}. """ - super(Flow, self).__init__(graph, value, cut, partition) + super().__init__(graph, value, cut, partition) self._flow = flow def __repr__(self): diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index 2a4e31fde..1a53bba07 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -5,7 +5,7 @@ from itertools import islice -class Matrix(object): +class Matrix: """Simple matrix data type. Of course there are much more advanced matrix data types for Python (for @@ -653,7 +653,7 @@ def __str__(self): return "\n".join(result) -class UniqueIdGenerator(object): +class UniqueIdGenerator: """A dictionary-like class that can be used to assign unique IDs to names (say, vertex names). diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 84fd3e4a7..244bb9116 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -42,7 +42,7 @@ ##################################################################### -class Plot(object): +class Plot: """Class representing an arbitrary plot Every plot has an associated surface object where the plotting is done. The diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index ddbc9b9a4..d434f7b75 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -8,7 +8,7 @@ ##################################################################### -class AbstractDrawer(object): +class AbstractDrawer: """Abstract class that serves as a base class for anything that draws an igraph object.""" diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index 4532fb173..78ecc275f 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -30,7 +30,7 @@ ) -class Palette(object): +class Palette: """Base class of color palettes. Color palettes are mappings that assign integers from the range diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 95e87b1c2..605b2df5c 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -20,7 +20,7 @@ cairo = find_cairo() -class AbstractEdgeDrawer(object): +class AbstractEdgeDrawer: """Abstract edge drawer object from which all concrete edge drawer implementations are derived.""" @@ -434,7 +434,7 @@ class AlphaVaryingEdgeDrawer(AbstractEdgeDrawer): """ def __init__(self, context, alpha_at_src, alpha_at_dest): - super(AlphaVaryingEdgeDrawer, self).__init__(context) + super().__init__(context) self.alpha_at_src = (clamp(float(alpha_at_src), 0.0, 1.0),) self.alpha_at_dest = (clamp(float(alpha_at_dest), 0.0, 1.0),) @@ -469,7 +469,7 @@ class LightToDarkEdgeDrawer(AlphaVaryingEdgeDrawer): """ def __init__(self, context): - super(LightToDarkEdgeDrawer, self).__init__(context, 0.0, 1.0) + super().__init__(context, 0.0, 1.0) class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): @@ -481,4 +481,4 @@ class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): """ def __init__(self, context): - super(DarkToLightEdgeDrawer, self).__init__(context, 1.0, 0.0) + super().__init__(context, 1.0, 0.0) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 5b1397013..c47b19193 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -543,7 +543,7 @@ class UbiGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): def __init__(self, url="http://localhost:20738/RPC2"): """Constructs an UbiGraph drawer using the display at the given URL.""" - super(UbiGraphDrawer, self).__init__(url, "ubigraph") + super().__init__(url, "ubigraph") self.vertex_defaults = dict(color="#ff0000", shape="cube", size=1.0) self.edge_defaults = dict(color="#ffffff", width=1.0) @@ -664,7 +664,7 @@ class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): def __init__(self, url="http://localhost:9000/Cytoscape"): """Constructs a Cytoscape graph drawer using the XML-RPC interface of Cytoscape at the given URL.""" - super(CytoscapeGraphDrawer, self).__init__(url, "Cytoscape") + super().__init__(url, "Cytoscape") self.network_id = None def draw(self, graph, name="Network from igraph", create_view=True, *args, **kwds): @@ -940,7 +940,7 @@ def __init__(self, conn=None, *args, **kwds): - C{GephiGraphStreamingDrawer(url="http://remote:1234/workspace7)} is the same as above, but with an explicit URL. """ - super(GephiGraphStreamingDrawer, self).__init__() + super().__init__() from igraph.remote.gephi import GephiGraphStreamer, GephiConnection diff --git a/src/igraph/drawing/metamagic.py b/src/igraph/drawing/metamagic.py index 29bdd2f22..a99684cdc 100644 --- a/src/igraph/drawing/metamagic.py +++ b/src/igraph/drawing/metamagic.py @@ -68,7 +68,7 @@ class VisualEdgeBuilder(AttributeCollectorBase): __all__ = ("AttributeSpecification", "AttributeCollectorBase") -class AttributeSpecification(object): +class AttributeSpecification: """Class that describes how the value of a given attribute should be retrieved. @@ -173,13 +173,13 @@ def __new__(mcs, name, bases, attrs): "%s.Element" % name, (attr_spec.name for attr_spec in attr_specs) ) - return super(AttributeCollectorMeta, mcs).__new__(mcs, name, bases, attrs) + return super().__new__(mcs, name, bases, attrs) @classmethod def record_generator(mcs, name, slots): """Generates a simple class that has the given slots and nothing else""" - class Element(object): + class Element: """A simple class that holds the attributes collected by the attribute collector""" diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index b81daf815..a868cbf4a 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -27,7 +27,7 @@ from igraph.utils import consecutive_pairs -class ShapeDrawer(object): +class ShapeDrawer: """Static class, the ancestor of all vertex shape drawer classes. Custom shapes must implement at least the C{draw_path} method of the class. @@ -293,7 +293,7 @@ def __init__(self, context, bbox=(1, 1), points=[]): @param bbox: ignored, leave it at its default value @param points: the list of corner points """ - super(PolygonDrawer, self).__init__(context, bbox) + super().__init__(context, bbox) self.points = points def draw_path(self, points=None, corner_radius=0): @@ -372,7 +372,7 @@ def draw(self, points=None): ##################################################################### -class ShapeDrawerDirectory(object): +class ShapeDrawerDirectory: """Static class that resolves shape names to their corresponding shape drawer classes. diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index 7d00624bd..eb248e5e7 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -14,7 +14,7 @@ ##################################################################### -class TextAlignment(object): +class TextAlignment: """Text alignment constants.""" LEFT, CENTER, RIGHT = "left", "center", "right" @@ -36,7 +36,7 @@ class TextDrawer(AbstractCairoDrawer): def __init__(self, context, text="", halign="center", valign="center"): """Constructs a new instance that will draw the given `text` on the given Cairo `context`.""" - super(TextDrawer, self).__init__(context, (0, 0)) + super().__init__(context, (0, 0)) self.text = text self.halign = halign self.valign = valign diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 31174fd73..ecf546320 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -11,7 +11,7 @@ ##################################################################### -class Rectangle(object): +class Rectangle: """Class representing a rectangle.""" __slots__ = ("_left", "_top", "_right", "_bottom") diff --git a/src/igraph/layout.py b/src/igraph/layout.py index eaf3e54c6..d6a45814e 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -13,7 +13,7 @@ from igraph.statistics import RunningMean -class Layout(object): +class Layout: """Represents the layout of a graph. A layout is practically a list of coordinates in an n-dimensional diff --git a/src/igraph/matching.py b/src/igraph/matching.py index 5ec97a456..afc966407 100644 --- a/src/igraph/matching.py +++ b/src/igraph/matching.py @@ -5,7 +5,7 @@ from igraph._igraph import Vertex -class Matching(object): +class Matching: """A matching of vertices in a graph. A matching of an undirected graph is a set of edges such that each diff --git a/src/igraph/remote/gephi.py b/src/igraph/remote/gephi.py index c75651935..aa3075cfb 100644 --- a/src/igraph/remote/gephi.py +++ b/src/igraph/remote/gephi.py @@ -11,7 +11,7 @@ __docformat__ = "restructuredtext en" -class GephiConnection(object): +class GephiConnection: """Object that represents a connection to a Gephi master server.""" def __init__(self, url=None, host="127.0.0.1", port=8080, workspace=1): @@ -79,7 +79,7 @@ def __repr__(self): return "%s(url=%r)" % (self.__class__.__name__, self.url) -class GephiGraphStreamingAPIFormat(object): +class GephiGraphStreamingAPIFormat: """Object that implements the Gephi graph streaming API format and returns Python objects corresponding to the events defined in the API. """ @@ -166,7 +166,7 @@ def get_delete_edge_event(self, identifier): return {"de": {identifier: {}}} -class GephiGraphStreamer(object): +class GephiGraphStreamer: """Class that produces JSON event objects that stream an igraph graph to Gephi using the Gephi Graph Streaming API. diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 7657ab6a4..68350b86a 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -18,7 +18,7 @@ ) -class FittedPowerLaw(object): +class FittedPowerLaw: """Result of fitting a power-law to a vector of samples Example: @@ -104,7 +104,7 @@ def summary(self, significance=0.05): return "\n".join(result) -class Histogram(object): +class Histogram: """Generic histogram class for real numbers Example: @@ -324,7 +324,7 @@ def __str__(self): return self.to_string() -class RunningMean(object): +class RunningMean: """Running mean calculator. This class can be used to calculate the mean of elements from a diff --git a/src/igraph/summary.py b/src/igraph/summary.py index 022f50479..fffb20726 100644 --- a/src/igraph/summary.py +++ b/src/igraph/summary.py @@ -13,7 +13,7 @@ __all__ = ("GraphSummary", "summary") -class FakeWrapper(object): +class FakeWrapper: """Object whose interface is compatible with C{textwrap.TextWrapper} but does no wrapping.""" @@ -38,7 +38,7 @@ def _get_wrapper_for_width(width, *args, **kwds): return TextWrapper(width=width, *args, **kwds) -class GraphSummary(object): +class GraphSummary: """Summary representation of a graph. The summary representation includes a header line and the list of diff --git a/tests/test_atlas.py b/tests/test_atlas.py index b1327550c..546a28300 100644 --- a/tests/test_atlas.py +++ b/tests/test_atlas.py @@ -3,7 +3,7 @@ from igraph import * -class AtlasTestBase(object): +class AtlasTestBase: def testPageRank(self): for idx, g in enumerate(self.__class__.graphs): try: diff --git a/tests/test_basic.py b/tests/test_basic.py index 28bca91e9..e21e67905 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -717,7 +717,7 @@ def testIsGraphicalSimple(self): class InheritedGraph(Graph): def __init__(self, *args, **kwds): - super(InheritedGraph, self).__init__(*args, **kwds) + super().__init__(*args, **kwds) self.init_called = True def __new__(cls, *args, **kwds): @@ -727,7 +727,7 @@ def __new__(cls, *args, **kwds): @classmethod def Adjacency(cls, *args, **kwds): - result = super(InheritedGraph, cls).Adjacency(*args, **kwds) + result = super().Adjacency(*args, **kwds) result.adjacency_called = True return result diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 0ef0c69ab..62017c17e 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -189,7 +189,7 @@ def testTriads(self): self.assertTrue(len(tuple(tc)) == 16) -class CliqueBenchmark(object): +class CliqueBenchmark: """This is a benchmark, not a real test case. You can run it using: diff --git a/tests/test_rng.py b/tests/test_rng.py index 00f82d8c4..b9ccc2707 100644 --- a/tests/test_rng.py +++ b/tests/test_rng.py @@ -3,7 +3,7 @@ from igraph import * -class FakeRNG(object): +class FakeRNG: @staticmethod def random(): return 0.1 @@ -17,7 +17,7 @@ def gauss(mu, sigma): return 0.3 -class InvalidRNG(object): +class InvalidRNG: pass From 98af0ee7d9e1198e6e3046b693a41aa1c5712040 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 10 Aug 2021 11:27:03 +1000 Subject: [PATCH 0335/1681] Converting types for test_basic.py --- src/_igraph/convert.c | 43 +++++++--- src/_igraph/convert.h | 2 +- src/_igraph/graphobject.c | 32 ++++---- src/_igraph/vertexseqobject.c | 147 +++++++++++++++++----------------- src/_igraph/vertexseqobject.h | 2 +- src/igraph/utils.py | 17 ++-- tox.ini | 2 +- 7 files changed, 137 insertions(+), 108 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 582800d38..63330e8a1 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -782,13 +782,36 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { return 0; } +/** + * \brief Converts a PyLong to an igraph \c igraph_integer_t + * + * Raises suitable Python exceptions when needed. + * + * This function differs from the next one because it is less generic, + * i.e. the Python object has to be a PyLong + * + * \param object the PyLong to be converted + * \param v the result is stored here + * \return 0 if everything was OK, 1 otherwise + */ +int PyLong_to_integer_t(PyObject* obj, int* v) { + if (IGRAPH_INTEGER_SIZE == 64) { + *v = (igraph_integer_t)PyLong_AsLong(obj); + } else { + int dummy; + PyLong_AsInt(obj, &dummy); + *v = (igraph_integer_t)dummy; + } + return 0; +} + /** * \brief Converts a Python object to an igraph \c igraph_integer_t * * Raises suitable Python exceptions when needed. * * \param object the Python object to be converted - * \param v the result is returned here + * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { @@ -796,16 +819,18 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { if (object == NULL) { } else if (PyLong_Check(object)) { - retval = PyLong_AsInt(object, &num); + retval = PyLong_to_integer_t(object, &num); if (retval) return retval; *v = num; return 0; } else if (PyNumber_Check(object)) { + /* try to recast as PyLong */ PyObject *i = PyNumber_Long(object); if (i == NULL) return 1; - retval = PyLong_AsInt(i, &num); + /* as above, plus decrement the reference for the temp variable */ + retval = PyLong_to_integer_t(i, &num); Py_DECREF(i); if (retval) return retval; @@ -1404,7 +1429,7 @@ PyObject* igraphmodule_vector_t_to_PyList_pairs(const igraph_vector_t *v) { * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_edgelist( - PyObject *list, igraph_vector_t *v, igraph_t *graph, + PyObject *list, igraph_vector_int_t *v, igraph_t *graph, igraph_bool_t* list_is_owned ) { PyObject *item, *i1, *i2, *it; @@ -1449,7 +1474,7 @@ int igraphmodule_PyObject_to_edgelist( return 1; } - igraph_vector_view(v, buffer->buf, buffer->len / buffer->itemsize); + igraph_vector_int_view(v, buffer->buf, buffer->len / buffer->itemsize); if (list_is_owned) { *list_is_owned = 0; @@ -1462,7 +1487,7 @@ int igraphmodule_PyObject_to_edgelist( if (!it) return 1; - igraph_vector_init(v, 0); + igraph_vector_int_init(v, 0); if (list_is_owned) { *list_is_owned = 1; } @@ -1488,18 +1513,18 @@ int igraphmodule_PyObject_to_edgelist( Py_DECREF(item); if (ok) { - if (igraph_vector_push_back(v, idx1)) { + if (igraph_vector_int_push_back(v, idx1)) { igraphmodule_handle_igraph_error(); ok = 0; } - if (ok && igraph_vector_push_back(v, idx2)) { + if (ok && igraph_vector_int_push_back(v, idx2)) { igraphmodule_handle_igraph_error(); ok = 0; } } if (!ok) { - igraph_vector_destroy(v); + igraph_vector_int_destroy(v); Py_DECREF(it); return 1; } diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 8da50789f..8472d97d3 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -96,7 +96,7 @@ int igraphmodule_PyObject_to_vector_ptr_t(PyObject *list, igraph_vector_ptr_t *v igraph_bool_t need_non_negative); int igraphmodule_PyObject_to_edgelist( - PyObject *list, igraph_vector_t *v, igraph_t *graph, + PyObject *list, igraph_vector_int_t *v, igraph_t *graph, igraph_bool_t *list_is_owned ); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6e38060a5..f95a69e84 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -204,7 +204,7 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, long int n = 0; PyObject *edges = NULL, *dir = Py_False, *ptr_o = 0; void* ptr = 0; - igraph_vector_t edges_vector; + igraph_vector_int_t edges_vector; igraph_bool_t edges_vector_owned = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOOO!", kwlist, @@ -232,7 +232,7 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, } } else if (edges) { /* Caller specified an edge list, so we use igraph_create */ - /* We have to convert the Python list to a igraph_vector_t */ + /* We have to convert the Python list to a igraph_vector_int_t */ if (igraphmodule_PyObject_to_edgelist(edges, &edges_vector, 0, &edges_vector_owned)) { igraphmodule_handle_igraph_error(); return -1; @@ -242,13 +242,13 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, (&self->g, &edges_vector, (igraph_integer_t) n, PyObject_IsTrue(dir))) { igraphmodule_handle_igraph_error(); if (edges_vector_owned) { - igraph_vector_destroy(&edges_vector); + igraph_vector_int_destroy(&edges_vector); } return -1; } if (edges_vector_owned) { - igraph_vector_destroy(&edges_vector); + igraph_vector_int_destroy(&edges_vector); } } else { /* No edge list was specified, and no previously initialized graph object @@ -7695,39 +7695,39 @@ PyObject *igraphmodule_Graph_get_incidence(igraphmodule_GraphObject * self, { static char *kwlist[] = { "types", NULL }; igraph_matrix_t matrix; - igraph_vector_t row_ids, col_ids; + igraph_vector_int_t row_ids, col_ids; igraph_vector_bool_t *types; PyObject *matrix_o, *row_ids_o, *col_ids_o, *types_o; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", kwlist, &types_o)) return NULL; - if (igraph_vector_init(&row_ids, 0)) + if (igraph_vector_int_init(&row_ids, 0)) return NULL; - if (igraph_vector_init(&col_ids, 0)) { - igraph_vector_destroy(&row_ids); + if (igraph_vector_int_init(&col_ids, 0)) { + igraph_vector_int_destroy(&row_ids); return NULL; } if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) { - igraph_vector_destroy(&row_ids); - igraph_vector_destroy(&col_ids); + igraph_vector_int_destroy(&row_ids); + igraph_vector_int_destroy(&col_ids); return NULL; } if (igraph_matrix_init(&matrix, 1, 1)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&row_ids); - igraph_vector_destroy(&col_ids); + igraph_vector_int_destroy(&row_ids); + igraph_vector_int_destroy(&col_ids); if (types) { igraph_vector_bool_destroy(types); free(types); } return NULL; } if (igraph_get_incidence(&self->g, types, &matrix, &row_ids, &col_ids)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&row_ids); - igraph_vector_destroy(&col_ids); + igraph_vector_int_destroy(&row_ids); + igraph_vector_int_destroy(&col_ids); if (types) { igraph_vector_bool_destroy(types); free(types); } igraph_matrix_destroy(&matrix); return NULL; @@ -7739,9 +7739,9 @@ PyObject *igraphmodule_Graph_get_incidence(igraphmodule_GraphObject * self, igraph_matrix_destroy(&matrix); row_ids_o = igraphmodule_vector_t_to_PyList(&row_ids, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&row_ids); + igraph_vector_int_destroy(&row_ids); col_ids_o = igraphmodule_vector_t_to_PyList(&col_ids, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&col_ids); + igraph_vector_int_destroy(&col_ids); return Py_BuildValue("NNN", matrix_o, row_ids_o, col_ids_o); } diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 77be5ce69..2dd2b8042 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -74,17 +74,17 @@ igraphmodule_VertexSeq_copy(igraphmodule_VertexSeqObject* o) { if (copy == NULL) return NULL; if (igraph_vs_type(&o->vs) == IGRAPH_VS_VECTOR) { - igraph_vector_t v; + igraph_vector_int_t v; if (igraph_vector_copy(&v, o->vs.data.vecptr)) { igraphmodule_handle_igraph_error(); return 0; } if (igraph_vs_vector_copy(©->vs, &v)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return 0; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } else { copy->vs = o->vs; } @@ -123,21 +123,21 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, } igraph_vs_1(&vs, (igraph_integer_t)idx); } else { - igraph_vector_t v; + igraph_vector_int_t v; igraph_integer_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); if (igraphmodule_PyObject_to_vector_t(vsobj, &v, 1)) return -1; - if (!igraph_vector_isininterval(&v, 0, n-1)) { - igraph_vector_destroy(&v); + if (!igraph_vector_int_isininterval(&v, 0, n-1)) { + igraph_vector_int_destroy(&v); PyErr_SetString(PyExc_ValueError, "vertex index out of range"); return -1; } if (igraph_vs_vector_copy(&vs, &v)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return -1; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } self->vs = vs; @@ -202,9 +202,9 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, case IGRAPH_VS_VECTOR: case IGRAPH_VS_VECTORPTR: if (i < 0) { - i = igraph_vector_size(self->vs.data.vecptr) + i; + i = igraph_vector_int_size(self->vs.data.vecptr) + i; } - if (i >= 0 && i < igraph_vector_size(self->vs.data.vecptr)) { + if (i >= 0 && i < igraph_vector_int_size(self->vs.data.vecptr)) { idx = (igraph_integer_t)VECTOR(*self->vs.data.vecptr)[i]; } break; @@ -278,7 +278,7 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje case IGRAPH_VS_VECTOR: case IGRAPH_VS_VECTORPTR: - n = igraph_vector_size(self->vs.data.vecptr); + n = igraph_vector_int_size(self->vs.data.vecptr); result = PyList_New(n); if (!result) return 0; @@ -343,7 +343,7 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values_mapping(igraphmodule_Verte int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqObject* self, PyObject* attrname, PyObject* values) { PyObject *dict, *list, *item; igraphmodule_GraphObject *gr; - igraph_vector_t vs; + igraph_vector_int_t vs; long i, j, n, no_of_nodes; gr = self->gref; @@ -421,19 +421,19 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb } else { /* We are working with a subset of the graph. Convert the sequence to a * vector and loop through it */ - if (igraph_vector_init(&vs, 0)) { + if (igraph_vector_int_init(&vs, 0)) { igraphmodule_handle_igraph_error(); return -1; } if (igraph_vs_as_vector(&gr->g, self->vs, &vs)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); return -1; } - no_of_nodes = (long)igraph_vector_size(&vs); + no_of_nodes = (long)igraph_vector_int_size(&vs); if (n == 0 && no_of_nodes > 0) { PyErr_SetString(PyExc_ValueError, "sequence must not be empty"); - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); return -1; } /* Check if we already have attributes with the given name */ @@ -444,17 +444,17 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb if (j == n) j = 0; item = PySequence_GetItem(values, j); if (item == 0) { - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); return -1; } /* No need to Py_INCREF(item), PySequence_GetItem returns a new reference */ if (PyList_SetItem(list, (long)VECTOR(vs)[i], item)) { Py_DECREF(item); - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); return -1; } /* PyList_SetItem stole a reference to the item automatically */ } - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); } else if (values != 0) { /* We don't have attributes with the given name yet. Create an entry * in the dict, create a new list, fill with None for vertices not in the @@ -462,7 +462,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb long n2 = igraph_vcount(&gr->g); list = PyList_New(n2); if (list == 0) { - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); return -1; } for (i=0; ivs); if (igraph_vs_vector_copy(&result->vs, &v)) { Py_DECREF(result); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); igraphmodule_handle_igraph_error(); return NULL; } } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } else if (PyLong_Check(item)) { /* Integers are treated specially: from now on, all remaining items * in the argument list must be integers and they will be used together * to restrict the vertex set. Integers are interpreted as indices on the * vertex set and NOT on the original, untouched vertex sequence of the * graph */ - igraph_vector_t v, v2; - if (igraph_vector_init(&v, 0)) { + igraph_vector_int_t v, v2; + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } if (igraph_vector_init(&v2, 0)) { - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); igraphmodule_handle_igraph_error(); return 0; } if (igraph_vs_as_vector(&gr->g, self->vs, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); igraphmodule_handle_igraph_error(); return 0; } - m = igraph_vector_size(&v2); + m = igraph_vector_int_size(&v2); for (; i= m || idx < 0) { PyErr_SetString(PyExc_ValueError, "vertex index out of range"); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[idx])) { + if (igraph_vector_int_push_back(&v, VECTOR(v2)[idx])) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } } - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v2); igraph_vs_destroy(&result->vs); if (igraph_vs_vector_copy(&result->vs, &v)) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return NULL; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } else { /* Iterators, slices and everything that was not handled directly */ PyObject *iter=0, *item2; - igraph_vector_t v, v2; + igraph_vector_int_t v, v2; /* Allocate stuff */ - if (igraph_vector_init(&v, 0)) { + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); Py_DECREF(result); return 0; } - if (igraph_vector_init(&v2, 0)) { - igraph_vector_destroy(&v); + if (igraph_vector_int_init(&v2, 0)) { + igraph_vector_int_destroy(&v); Py_DECREF(result); igraphmodule_handle_igraph_error(); return 0; } + if (igraph_vs_as_vector(&gr->g, self->vs, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); Py_DECREF(result); igraphmodule_handle_igraph_error(); return 0; } - m = igraph_vector_size(&v2); + m = igraph_vector_int_size(&v2); /* Create an appropriate iterator */ if (PySlice_Check(item)) { @@ -740,9 +741,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject* range; igraph_bool_t ok; - /* Casting to void* because Python 2.x expects PySliceObject* - * but Python 3.x expects PyObject* */ - ok = (PySlice_GetIndicesEx((void*)item, igraph_vector_size(&v2), + ok = (PySlice_GetIndicesEx(item, igraph_vector_int_size(&v2), &start, &stop, &step, &sl) == 0); if (ok) { @@ -755,8 +754,8 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, ok = (iter != 0); } if (!ok) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); PyErr_SetString(PyExc_TypeError, "error while converting slice to iterator"); Py_DECREF(result); return 0; @@ -768,8 +767,8 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, /* Did we manage to get an iterator? */ if (iter == 0) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); PyErr_SetString(PyExc_TypeError, "invalid vertex filter among positional arguments"); Py_DECREF(result); return 0; @@ -785,25 +784,25 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyErr_SetString(PyExc_ValueError, "vertex index out of range"); Py_DECREF(result); Py_DECREF(iter); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[(long int) igraph_idx])) { + if (igraph_vector_int_push_back(&v, VECTOR(v2)[(long int) igraph_idx])) { Py_DECREF(result); Py_DECREF(iter); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } } } /* Deallocate stuff */ - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v2); Py_DECREF(iter); if (PyErr_Occurred()) { - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); Py_DECREF(result); return 0; } @@ -811,10 +810,10 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, if (igraph_vs_vector_copy(&result->vs, &v)) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return NULL; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } } @@ -828,7 +827,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, * \return 0 if everything was ok, 1 otherwise */ int igraphmodule_VertexSeq_to_vector_t(igraphmodule_VertexSeqObject *self, - igraph_vector_t *v) { + igraph_vector_int_t *v) { return igraph_vs_as_vector(&self->gref->g, self->vs, v); } @@ -849,21 +848,21 @@ PyObject* igraphmodule_VertexSeq_get_graph(igraphmodule_VertexSeqObject* self, PyObject* igraphmodule_VertexSeq_get_indices(igraphmodule_VertexSeqObject* self, void* closure) { igraphmodule_GraphObject *gr = self->gref; - igraph_vector_t vs; + igraph_vector_int_t vs; PyObject *result; - if (igraph_vector_init(&vs, 0)) { + if (igraph_vector_int_init(&vs, 0)) { igraphmodule_handle_igraph_error(); return 0; } if (igraph_vs_as_vector(&gr->g, self->vs, &vs)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&vs); + igraph_vector_int_destroy(&vs); return 0; } - result = igraphmodule_vector_t_to_PyList(&vs, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&vs); + result = igraphmodule_vector_int_t_to_PyList(&vs); + igraph_vector_int_destroy(&vs); return result; } diff --git a/src/_igraph/vertexseqobject.h b/src/_igraph/vertexseqobject.h index 987f1f768..708889a97 100644 --- a/src/_igraph/vertexseqobject.h +++ b/src/_igraph/vertexseqobject.h @@ -51,7 +51,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject *args); int igraphmodule_VertexSeq_to_vector_t(igraphmodule_VertexSeqObject *self, - igraph_vector_t *v); + igraph_vector_int_t *v); PyObject* igraphmodule_VertexSeq_get_graph(igraphmodule_VertexSeqObject *self, void* closure); diff --git a/src/igraph/utils.py b/src/igraph/utils.py index 4b86e5dd6..8cc381cb4 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from collections.abc import MutableMapping -from ctypes import c_double, sizeof +from ctypes import sizeof from itertools import chain import os @@ -59,13 +59,18 @@ def numpy_to_contiguous_memoryview(obj): directly when constructing a Graph. """ # Deferred import to prevent a hard dependency on NumPy - from numpy import float32, float64, require - - size = sizeof(c_double) + from numpy import int32, int64, require + + # TODO: we used to export to double, which is only dependent on the + # architecture. Now with integers and a compile-time flag, we have + # to figure out what is the integer bitness of the underlying C core. + # Think of how to do that, for now default to 64 bit ints! + size = 8 + #size = sizeof(c_double) if size == 8: - dtype = float64 + dtype = int64 elif size == 4: - dtype = float32 + dtype = int32 else: raise TypeError("size of C double (%d bytes) is not supported" % size) diff --git a/tox.ini b/tox.ini index bd0355f4d..03eb48125 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ python = pypy-3.7: pypy3 [testenv] -commands = python -m unittest +commands = python -m unittest {posargs} deps = scipy; platform_python_implementation != "PyPy" numpy; platform_python_implementation != "PyPy" From 1c8c276884b643edca8bf702f4ea77692f830ca6 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 10 Aug 2021 15:27:38 +1000 Subject: [PATCH 0336/1681] More work on vertices, edges, and attributes --- src/_igraph/attributes.c | 56 +++++++++++++++++------------------ src/_igraph/convert.c | 56 +++++++++++++++++------------------ src/_igraph/convert.h | 6 ++-- src/_igraph/graphobject.c | 26 ++++++++-------- src/_igraph/vertexseqobject.c | 4 +-- 5 files changed, 73 insertions(+), 75 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 7c4eb8749..fa7bdd3ed 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -553,7 +553,7 @@ static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, long int nv, i /* Permuting vertices */ static int igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, - igraph_t *newgraph, const igraph_vector_t *idx) { + igraph_t *newgraph, const igraph_vector_int_t *idx) { long int n, i; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos=0; @@ -564,7 +564,7 @@ static int igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, newdict=PyDict_New(); if (!newdict) return 1; - n=igraph_vector_size(idx); + n=igraph_vector_int_size(idx); pos=0; while (PyDict_Next(dict, &pos, &key, &value)) { @@ -593,7 +593,7 @@ static int igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, } /* Adding edges */ -static int igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vector_t *edges, igraph_vector_ptr_t *attr) { +static int igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vector_int_t *edges, igraph_vector_ptr_t *attr) { /* Extend the end of every value in the edge hash with ne pieces of None */ PyObject *key, *value, *dict; Py_ssize_t pos=0; @@ -601,7 +601,7 @@ static int igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vect igraph_bool_t *added_attrs=0; igraph_attribute_record_t *attr_rec; - ne=igraph_vector_size(edges)/2; + ne=igraph_vector_int_size(edges)/2; if (!graph->attr) return IGRAPH_SUCCESS; if (ne<0) return IGRAPH_SUCCESS; @@ -771,7 +771,7 @@ static void igraphmodule_i_attribute_delete_edges(igraph_t *graph, const igraph_ /* Permuting edges */ static int igraphmodule_i_attribute_permute_edges(const igraph_t *graph, - igraph_t *newgraph, const igraph_vector_t *idx) { + igraph_t *newgraph, const igraph_vector_int_t *idx) { long int n, i; PyObject *key, *value, *dict, *newdict, *newlist, *o; Py_ssize_t pos=0; @@ -782,7 +782,7 @@ static int igraphmodule_i_attribute_permute_edges(const igraph_t *graph, newdict=PyDict_New(); if (!newdict) return 1; - n=igraph_vector_size(idx); + n=igraph_vector_int_size(idx); pos=0; while (PyDict_Next(dict, &pos, &key, &value)) { @@ -822,8 +822,8 @@ static PyObject* igraphmodule_i_ac_func(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; - long int j, n = igraph_vector_size(v); + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; + long int j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { @@ -889,9 +889,9 @@ static PyObject* igraphmodule_i_ac_sum(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; igraph_real_t num = 0.0, sum = 0.0; - long int j, n = igraph_vector_size(v); + long int j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -923,9 +923,9 @@ static PyObject* igraphmodule_i_ac_prod(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; igraph_real_t num = 1.0, prod = 1.0; - long int j, n = igraph_vector_size(v); + long int j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -957,8 +957,8 @@ static PyObject* igraphmodule_i_ac_first(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; - long int n = igraph_vector_size(v); + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; + long int n = igraph_vector_int_size(v); item = n > 0 ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; Py_INCREF(item); @@ -992,8 +992,8 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; - long int n = igraph_vector_size(v); + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; + long int n = igraph_vector_int_size(v); if (n > 0) { num = PyObject_CallObject(random_func, 0); @@ -1030,8 +1030,8 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; - long int n = igraph_vector_size(v); + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; + long int n = igraph_vector_int_size(v); item = (n > 0) ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; Py_INCREF(item); @@ -1054,9 +1054,9 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; igraph_real_t num = 0.0, mean = 0.0; - long int j, n = igraph_vector_size(v); + long int j, n = igraph_vector_int_size(v); for (j = 0; j < n; ) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1090,8 +1090,8 @@ static PyObject* igraphmodule_i_ac_median(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { - igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; - long int j, n = igraph_vector_size(v); + igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; + long int j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1317,19 +1317,19 @@ static int igraphmodule_i_attribute_combine_edges(const igraph_t *graph, /* Getting attribute names and types */ static int igraphmodule_i_attribute_get_info(const igraph_t *graph, igraph_strvector_t *gnames, - igraph_vector_t *gtypes, + igraph_vector_int_t *gtypes, igraph_strvector_t *vnames, - igraph_vector_t *vtypes, + igraph_vector_int_t *vtypes, igraph_strvector_t *enames, - igraph_vector_t *etypes) { + igraph_vector_int_t *etypes) { igraph_strvector_t *names[3] = { gnames, vnames, enames }; - igraph_vector_t *types[3] = { gtypes, vtypes, etypes }; + igraph_vector_int_t *types[3] = { gtypes, vtypes, etypes }; int retval; long int i, j, k, l, m; for (i=0; i<3; i++) { igraph_strvector_t *n = names[i]; - igraph_vector_t *t = types[i]; + igraph_vector_int_t *t = types[i]; PyObject *dict = ATTR_STRUCT_DICT(graph)[i]; PyObject *keys; PyObject *values; @@ -1344,7 +1344,7 @@ static int igraphmodule_i_attribute_get_info(const igraph_t *graph, } if (t) { k=PyList_Size(keys); - igraph_vector_resize(t, k); + igraph_vector_int_resize(t, k); for (j=0; j 0) { + if (igraph_vector_int_size(&vector) > 0) { igraph_es_vector_copy(es, &vector); } else { igraph_es_none(es); } - igraph_vector_destroy(&vector); + igraph_vector_int_destroy(&vector); if (return_single) { *return_single = 0; diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 8472d97d3..e0edcb29c 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -144,9 +144,9 @@ PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v); PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, igraphmodule_conv_t type); PyObject* igraphmodule_vector_t_to_PyTuple(const igraph_vector_t *v); -PyObject* igraphmodule_vector_t_pair_to_PyList(const igraph_vector_t *v1, - const igraph_vector_t *v2); -PyObject* igraphmodule_vector_t_to_PyList_pairs(const igraph_vector_t *v); +PyObject* igraphmodule_vector_int_t_pair_to_PyList(const igraph_vector_int_t *v1, + const igraph_vector_int_t *v2); +PyObject* igraphmodule_vector_int_t_to_PyList_pairs(const igraph_vector_int_t *v); PyObject* igraphmodule_vector_ptr_t_to_PyList(const igraph_vector_ptr_t *v, igraphmodule_conv_t type); PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index f95a69e84..88ea1600a 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -638,7 +638,7 @@ PyObject *igraphmodule_Graph_add_edges(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { PyObject *list; - igraph_vector_t v; + igraph_vector_int_t v; igraph_bool_t v_owned = 0; if (!PyArg_ParseTuple(args, "O", &list)) @@ -651,13 +651,13 @@ PyObject *igraphmodule_Graph_add_edges(igraphmodule_GraphObject * self, if (igraph_add_edges(&self->g, &v, 0)) { igraphmodule_handle_igraph_error(); if (v_owned) { - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } return NULL; } if (v_owned) { - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } Py_RETURN_NONE; @@ -3064,7 +3064,7 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, CREATE_GRAPH_FROM_TYPE(self, g, type); if (store_attribs) { - type_vec_o = igraphmodule_vector_t_pair_to_PyList(&in_type_vec, + type_vec_o = igraphmodule_vector_int_t_pair_to_PyList(&in_type_vec, &out_type_vec); if (type_vec_o == NULL) { igraph_matrix_destroy(&pm); @@ -5488,23 +5488,23 @@ PyObject *igraphmodule_Graph_permute_vertices(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "permutation", NULL }; igraph_t pg; - igraph_vector_t perm; + igraph_vector_int_t perm; igraphmodule_GraphObject *result; PyObject *list; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!", kwlist, &PyList_Type, &list)) return NULL; - if (igraphmodule_PyObject_to_vector_t(list, &perm, 1)) + if (igraphmodule_PyObject_to_vector_int_t(list, &perm)) return NULL; if (igraph_permute_vertices(&self->g, &pg, &perm)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&perm); + igraph_vector_int_destroy(&perm); return NULL; } - igraph_vector_destroy(&perm); + igraph_vector_int_destroy(&perm); CREATE_GRAPH(result, pg); @@ -7804,18 +7804,18 @@ PyObject *igraphmodule_Graph_laplacian(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_get_edgelist(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - igraph_vector_t edgelist; + igraph_vector_int_t edgelist; PyObject *result; - igraph_vector_init(&edgelist, igraph_ecount(&self->g)); + igraph_vector_int_init(&edgelist, igraph_ecount(&self->g)); if (igraph_get_edgelist(&self->g, &edgelist, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&edgelist); + igraph_vector_int_destroy(&edgelist); return NULL; } - result = igraphmodule_vector_t_to_PyList_pairs(&edgelist); - igraph_vector_destroy(&edgelist); + result = igraphmodule_vector_int_t_to_PyList_pairs(&edgelist); + igraph_vector_int_destroy(&edgelist); return (PyObject *) result; } diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 2dd2b8042..7ee4fcde4 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -125,7 +125,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, } else { igraph_vector_int_t v; igraph_integer_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); - if (igraphmodule_PyObject_to_vector_t(vsobj, &v, 1)) + if (igraphmodule_PyObject_to_vector_int_t(vsobj, &v)) return -1; if (!igraph_vector_int_isininterval(&v, 0, n-1)) { igraph_vector_int_destroy(&v); @@ -661,7 +661,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, igraphmodule_handle_igraph_error(); return 0; } - if (igraph_vector_init(&v2, 0)) { + if (igraph_vector_int_init(&v2, 0)) { igraph_vector_int_destroy(&v); igraphmodule_handle_igraph_error(); return 0; From 8781af57a63079f68acc3a649e0b945d9036a8c2 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 10 Aug 2021 15:41:32 +1000 Subject: [PATCH 0337/1681] Edge sequences --- src/_igraph/edgeseqobject.c | 150 ++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 76 deletions(-) diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index dc7457784..cf6955fe4 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -74,17 +74,17 @@ igraphmodule_EdgeSeq_copy(igraphmodule_EdgeSeqObject* o) { if (copy == NULL) return NULL; if (igraph_es_type(&o->es) == IGRAPH_ES_VECTOR) { - igraph_vector_t v; - if (igraph_vector_copy(&v, o->es.data.vecptr)) { + igraph_vector_int_t v; + if (igraph_vector_int_copy(&v, o->es.data.vecptr)) { igraphmodule_handle_igraph_error(); return 0; } if (igraph_es_vector_copy(©->es, &v)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return 0; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } else { copy->es = o->es; } @@ -125,21 +125,21 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, igraph_es_1(&es, (igraph_integer_t)idx); } else { /* We selected multiple edges */ - igraph_vector_t v; + igraph_vector_int_t v; igraph_integer_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); - if (igraphmodule_PyObject_to_vector_t(esobj, &v, 1)) + if (igraphmodule_PyObject_to_vector_int_t(esobj, &v)) return -1; - if (!igraph_vector_isininterval(&v, 0, n-1)) { - igraph_vector_destroy(&v); + if (!igraph_vector_int_isininterval(&v, 0, n-1)) { + igraph_vector_int_destroy(&v); PyErr_SetString(PyExc_ValueError, "edge index out of range"); return -1; } if (igraph_es_vector_copy(&es, &v)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return -1; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } self->es = es; @@ -206,9 +206,9 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, case IGRAPH_ES_VECTOR: case IGRAPH_ES_VECTORPTR: if (i < 0) { - i = igraph_vector_size(self->es.data.vecptr) + i; + i = igraph_vector_int_size(self->es.data.vecptr) + i; } - if (i >= 0 && i < igraph_vector_size(self->es.data.vecptr)) { + if (i >= 0 && i < igraph_vector_int_size(self->es.data.vecptr)) { idx = (igraph_integer_t)VECTOR(*self->es.data.vecptr)[i]; } break; @@ -285,7 +285,7 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* case IGRAPH_ES_VECTOR: case IGRAPH_ES_VECTORPTR: - n = igraph_vector_size(self->es.data.vecptr); + n = igraph_vector_int_size(self->es.data.vecptr); result = PyList_New(n); if (!result) return 0; @@ -355,7 +355,7 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values_mapping(igraphmodule_EdgeSeq int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject* self, PyObject* attrname, PyObject* values) { PyObject *dict, *list, *item; igraphmodule_GraphObject *gr; - igraph_vector_t es; + igraph_vector_int_t es; long i, j, n, no_of_edges; gr = self->gref; @@ -430,19 +430,19 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject } else { /* We are working with a subset of the graph. Convert the sequence to a * vector and loop through it */ - if (igraph_vector_init(&es, 0)) { + if (igraph_vector_int_init(&es, 0)) { igraphmodule_handle_igraph_error(); return -1; } if (igraph_es_as_vector(&gr->g, self->es, &es)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&es); + igraph_vector_int_destroy(&es); return -1; } - no_of_edges = (long)igraph_vector_size(&es); + no_of_edges = (long)igraph_vector_int_size(&es); if (n == 0 && no_of_edges > 0) { PyErr_SetString(PyExc_ValueError, "sequence must not be empty"); - igraph_vector_destroy(&es); + igraph_vector_int_destroy(&es); return -1; } /* Check if we already have attributes with the given name */ @@ -452,22 +452,22 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject for (i=0, j=0; ig); list = PyList_New(n2); - if (list == 0) { igraph_vector_destroy(&es); return -1; } + if (list == 0) { igraph_vector_int_destroy(&es); return -1; } for (i=0; ies); if (igraph_es_vector_copy(&result->es, &v)) { Py_DECREF(result); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); igraphmodule_handle_igraph_error(); return NULL; } } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } else if (PyLong_Check(item)) { /* Integers are treated specially: from now on, all remaining items * in the argument list must be integers and they will be used together * to restrict the edge set. Integers are interpreted as indices on the * edge set and NOT on the original, untouched edge sequence of the * graph */ - igraph_vector_t v, v2; - if (igraph_vector_init(&v, 0)) { + igraph_vector_int_t v, v2; + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } - if (igraph_vector_init(&v2, 0)) { - igraph_vector_destroy(&v); + if (igraph_vector_int_init(&v2, 0)) { + igraph_vector_int_destroy(&v); igraphmodule_handle_igraph_error(); return 0; } if (igraph_es_as_vector(&gr->g, self->es, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); igraphmodule_handle_igraph_error(); return 0; } - m = igraph_vector_size(&v2); + m = igraph_vector_int_size(&v2); for (; i= m || idx < 0) { PyErr_SetString(PyExc_ValueError, "edge index out of range"); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[idx])) { + if (igraph_vector_int_push_back(&v, VECTOR(v2)[idx])) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } } - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v2); igraph_es_destroy(&result->es); if (igraph_es_vector_copy(&result->es, &v)) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return NULL; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } else { /* Iterators and everything that was not handled directly */ PyObject *iter, *item2; - igraph_vector_t v, v2; + igraph_vector_int_t v, v2; /* Allocate stuff */ - if (igraph_vector_init(&v, 0)) { + if (igraph_vector_int_init(&v, 0)) { igraphmodule_handle_igraph_error(); return 0; } - if (igraph_vector_init(&v2, 0)) { - igraph_vector_destroy(&v); + if (igraph_vector_int_init(&v2, 0)) { + igraph_vector_int_destroy(&v); igraphmodule_handle_igraph_error(); return 0; } if (igraph_es_as_vector(&gr->g, self->es, &v2)) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); igraphmodule_handle_igraph_error(); return 0; } - m = igraph_vector_size(&v2); + m = igraph_vector_int_size(&v2); /* Create an appropriate iterator */ if (PySlice_Check(item)) { @@ -708,9 +708,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject PyObject* range; igraph_bool_t ok; - /* Casting to void* because Python 2.x expects PySliceObject* - * but Python 3.x expects PyObject* */ - ok = (PySlice_GetIndicesEx((void*)item, igraph_vector_size(&v2), + ok = (PySlice_GetIndicesEx(item, igraph_vector_int_size(&v2), &start, &stop, &step, &sl) == 0); if (ok) { range = igraphmodule_PyRange_create(start, stop, step); @@ -722,8 +720,8 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject ok = (iter != 0); } if (!ok) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); PyErr_SetString(PyExc_TypeError, "error while converting slice to iterator"); Py_DECREF(result); return 0; @@ -735,8 +733,8 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject /* Did we manage to get an iterator? */ if (iter == 0) { - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); PyErr_SetString(PyExc_TypeError, "invalid edge filter among positional arguments"); Py_DECREF(result); return 0; @@ -752,25 +750,25 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject PyErr_SetString(PyExc_ValueError, "edge index out of range"); Py_DECREF(result); Py_DECREF(iter); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } - if (igraph_vector_push_back(&v, VECTOR(v2)[(long int) igraph_idx])) { + if (igraph_vector_int_push_back(&v, VECTOR(v2)[(long int) igraph_idx])) { Py_DECREF(result); Py_DECREF(iter); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v); + igraph_vector_int_destroy(&v2); return NULL; } } } /* Deallocate stuff */ - igraph_vector_destroy(&v2); + igraph_vector_int_destroy(&v2); Py_DECREF(iter); if (PyErr_Occurred()) { - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); Py_DECREF(result); return 0; } @@ -778,10 +776,10 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject if (igraph_es_vector_copy(&result->es, &v)) { Py_DECREF(result); igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); return NULL; } - igraph_vector_destroy(&v); + igraph_vector_int_destroy(&v); } } @@ -881,21 +879,21 @@ PyObject* igraphmodule_EdgeSeq_get_graph(igraphmodule_EdgeSeqObject* self, PyObject* igraphmodule_EdgeSeq_get_indices(igraphmodule_EdgeSeqObject* self, void* closure) { igraphmodule_GraphObject *gr = self->gref; - igraph_vector_t es; + igraph_vector_int_t es; PyObject *result; - if (igraph_vector_init(&es, 0)) { + if (igraph_vector_int_init(&es, 0)) { igraphmodule_handle_igraph_error(); return 0; } if (igraph_es_as_vector(&gr->g, self->es, &es)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&es); + igraph_vector_int_destroy(&es); return 0; } - result = igraphmodule_vector_t_to_PyList(&es, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&es); + result = igraphmodule_vector_int_t_to_PyList(&es); + igraph_vector_int_destroy(&es); return result; } From 570a953c24925f24e9baa5c621fe62897f8e801a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 10 Aug 2021 17:37:24 +1000 Subject: [PATCH 0338/1681] Passes test_basic --- src/_igraph/graphobject.c | 87 +++++++++++++++++++------------------- src/_igraph/igraphmodule.c | 48 ++++++++++----------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 88ea1600a..3fb559fcf 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1158,7 +1158,7 @@ PyObject *igraphmodule_Graph_has_multiple(igraphmodule_GraphObject *self) { PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { PyObject *list = Py_None; - igraph_vector_t result; + igraph_vector_int_t result; igraph_es_t es; igraph_bool_t return_single = 0; @@ -1172,7 +1172,7 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, return NULL; } - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&result, 0)) { igraph_es_destroy(&es); return NULL; } @@ -1180,12 +1180,12 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, if (igraph_count_multiple(&self->g, &result, es)) { igraphmodule_handle_igraph_error(); igraph_es_destroy(&es); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); + list = igraphmodule_vector_int_t_to_PyList(&result); else list = PyLong_FromLong((long int)VECTOR(result)[0]); @@ -1212,7 +1212,7 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject *list, *dtype_o=Py_None, *dmode_o=Py_None, *index_o; igraph_neimode_t dmode = IGRAPH_ALL; igraph_integer_t idx; - igraph_vector_t result; + igraph_vector_int_t result; static char *kwlist[] = { "vertex", "mode", "type", NULL }; @@ -1231,17 +1231,17 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) return NULL; - if (igraph_vector_init(&result, 1)) + if (igraph_vector_int_init(&result, 1)) return igraphmodule_handle_igraph_error(); if (igraph_neighbors(&self->g, &result, idx, dmode)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return list; } @@ -1263,7 +1263,7 @@ PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, PyObject *list, *dmode_o = Py_None, *dtype_o = Py_None, *index_o; igraph_neimode_t dmode = IGRAPH_OUT; igraph_integer_t idx; - igraph_vector_t result; + igraph_vector_int_t result; static char *kwlist[] = { "vertex", "mode", "type", NULL }; @@ -1282,15 +1282,15 @@ PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) return NULL; - igraph_vector_init(&result, 1); + igraph_vector_int_init(&result, 1); if (igraph_incident(&self->g, &result, idx, dmode)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return list; } @@ -1336,7 +1336,7 @@ PyObject *igraphmodule_Graph_successors(igraphmodule_GraphObject * self, { PyObject *list, *index_o; igraph_integer_t idx; - igraph_vector_t result; + igraph_vector_int_t result; static char *kwlist[] = { "vertex", NULL }; @@ -1346,15 +1346,15 @@ PyObject *igraphmodule_Graph_successors(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) return NULL; - igraph_vector_init(&result, 1); + igraph_vector_int_init(&result, 1); if (igraph_neighbors(&self->g, &result, idx, IGRAPH_OUT)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return list; } @@ -1373,7 +1373,7 @@ PyObject *igraphmodule_Graph_predecessors(igraphmodule_GraphObject * self, { PyObject *list, *index_o; igraph_integer_t idx; - igraph_vector_t result; + igraph_vector_int_t result; static char *kwlist[] = { "vertex", NULL }; @@ -1383,15 +1383,15 @@ PyObject *igraphmodule_Graph_predecessors(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) return NULL; - igraph_vector_init(&result, 1); + igraph_vector_int_init(&result, 1); if (igraph_neighbors(&self->g, &result, idx, IGRAPH_IN)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return list; } @@ -1498,7 +1498,7 @@ PyObject *igraphmodule_Graph_get_eids(igraphmodule_GraphObject * self, PyObject *directed = Py_True; PyObject *error = Py_True; PyObject *result = NULL; - igraph_vector_t pairs, path, res; + igraph_vector_int_t pairs, path, res; igraph_bool_t pairs_owned = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, @@ -1506,21 +1506,21 @@ PyObject *igraphmodule_Graph_get_eids(igraphmodule_GraphObject * self, &error)) return NULL; - if (igraph_vector_init(&res, 0)) + if (igraph_vector_int_init(&res, 0)) return igraphmodule_handle_igraph_error(); if (pairs_o != Py_None) { if (igraphmodule_PyObject_to_edgelist(pairs_o, &pairs, &self->g, &pairs_owned)) { - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return NULL; } } if (path_o != Py_None) { - if (igraphmodule_PyObject_to_vector_t(path_o, &path, 1)) { - igraph_vector_destroy(&res); + if (igraphmodule_PyObject_to_vector_int_t(path_o, &path)) { + igraph_vector_int_destroy(&res); if (pairs_owned) { - igraph_vector_destroy(&pairs); + igraph_vector_int_destroy(&pairs); } return NULL; } @@ -1532,24 +1532,24 @@ PyObject *igraphmodule_Graph_get_eids(igraphmodule_GraphObject * self, PyObject_IsTrue(directed), PyObject_IsTrue(error))) { if (pairs_owned) { - igraph_vector_destroy(&pairs); + igraph_vector_int_destroy(&pairs); } if (path_o != Py_None) { - igraph_vector_destroy(&path); + igraph_vector_int_destroy(&path); } - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return igraphmodule_handle_igraph_error(); } if (pairs_owned) { - igraph_vector_destroy(&pairs); + igraph_vector_int_destroy(&pairs); } if (path_o != Py_None) { - igraph_vector_destroy(&path); + igraph_vector_int_destroy(&path); } - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); + result = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return result; } @@ -5270,10 +5270,9 @@ PyObject *igraphmodule_Graph_neighborhood(igraphmodule_GraphObject *self, if (!return_single) result = igraphmodule_vector_ptr_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); else - result = igraphmodule_vector_t_to_PyList((igraph_vector_t*)VECTOR(res)[0], - IGRAPHMODULE_TYPE_INT); + result = igraphmodule_vector_int_t_to_PyList((igraph_vector_int_t*)VECTOR(res)[0]); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&res, igraph_vector_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&res, igraph_vector_int_destroy); igraph_vector_ptr_destroy_all(&res); return result; @@ -5296,7 +5295,7 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, igraph_neimode_t mode = IGRAPH_ALL; igraph_bool_t return_single = 0; igraph_vs_t vs; - igraph_vector_t res; + igraph_vector_int_t res; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlOi", kwlist, &vobj, &order, &mode_o, &mindist)) @@ -5309,7 +5308,7 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&res, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraph_vs_destroy(&vs); return igraphmodule_handle_igraph_error(); } @@ -5323,11 +5322,11 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, igraph_vs_destroy(&vs); if (!return_single) - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); + result = igraphmodule_vector_int_t_to_PyList(&res); else result = PyLong_FromLong((long)VECTOR(res)[0]); - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return result; } diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index ea3661c3f..d19883e0c 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -413,7 +413,7 @@ PyObject* igraphmodule_is_degree_sequence(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "out_deg", "in_deg", NULL }; PyObject *out_deg_o = 0, *in_deg_o = 0; - igraph_vector_t out_deg, in_deg; + igraph_vector_int_t out_deg, in_deg; igraph_bool_t is_directed, result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, @@ -422,25 +422,25 @@ PyObject* igraphmodule_is_degree_sequence(PyObject *self, is_directed = (in_deg_o != 0 && in_deg_o != Py_None); - if (igraphmodule_PyObject_to_vector_t(out_deg_o, &out_deg, 0)) + if (igraphmodule_PyObject_to_vector_int_t(out_deg_o, &out_deg)) return NULL; - if (is_directed && igraphmodule_PyObject_to_vector_t(in_deg_o, &in_deg, 0)) { - igraph_vector_destroy(&out_deg); + if (is_directed && igraphmodule_PyObject_to_vector_int_t(in_deg_o, &in_deg)) { + igraph_vector_int_destroy(&out_deg); return NULL; } if (igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, IGRAPH_LOOPS_SW | IGRAPH_MULTI_SW, &result)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&out_deg); + igraph_vector_int_destroy(&out_deg); if (is_directed) - igraph_vector_destroy(&in_deg); + igraph_vector_int_destroy(&in_deg); return NULL; } - igraph_vector_destroy(&out_deg); + igraph_vector_int_destroy(&out_deg); if (is_directed) - igraph_vector_destroy(&in_deg); + igraph_vector_int_destroy(&in_deg); if (result) Py_RETURN_TRUE; @@ -453,7 +453,7 @@ PyObject* igraphmodule_is_graphical_degree_sequence(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "out_deg", "in_deg", NULL }; PyObject *out_deg_o = 0, *in_deg_o = 0; - igraph_vector_t out_deg, in_deg; + igraph_vector_int_t out_deg, in_deg; igraph_bool_t is_directed, result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, @@ -462,25 +462,25 @@ PyObject* igraphmodule_is_graphical_degree_sequence(PyObject *self, is_directed = (in_deg_o != 0 && in_deg_o != Py_None); - if (igraphmodule_PyObject_to_vector_t(out_deg_o, &out_deg, 0)) + if (igraphmodule_PyObject_to_vector_int_t(out_deg_o, &out_deg)) return NULL; - if (is_directed && igraphmodule_PyObject_to_vector_t(in_deg_o, &in_deg, 0)) { - igraph_vector_destroy(&out_deg); + if (is_directed && igraphmodule_PyObject_to_vector_int_t(in_deg_o, &in_deg)) { + igraph_vector_int_destroy(&out_deg); return NULL; } if (igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, IGRAPH_SIMPLE_SW, &result)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&out_deg); + igraph_vector_int_destroy(&out_deg); if (is_directed) - igraph_vector_destroy(&in_deg); + igraph_vector_int_destroy(&in_deg); return NULL; } - igraph_vector_destroy(&out_deg); + igraph_vector_int_destroy(&out_deg); if (is_directed) - igraph_vector_destroy(&in_deg); + igraph_vector_int_destroy(&in_deg); if (result) Py_RETURN_TRUE; @@ -493,7 +493,7 @@ PyObject* igraphmodule_is_graphical(PyObject *self, PyObject *args, PyObject *kw static char* kwlist[] = { "out_deg", "in_deg", "loops", "multiple", NULL }; PyObject *out_deg_o = 0, *in_deg_o = 0; PyObject *loops = Py_False, *multiple = Py_False; - igraph_vector_t out_deg, in_deg; + igraph_vector_int_t out_deg, in_deg; igraph_bool_t is_directed, result; int allowed_edge_types; @@ -503,11 +503,11 @@ PyObject* igraphmodule_is_graphical(PyObject *self, PyObject *args, PyObject *kw is_directed = (in_deg_o != 0 && in_deg_o != Py_None); - if (igraphmodule_PyObject_to_vector_t(out_deg_o, &out_deg, 0)) + if (igraphmodule_PyObject_to_vector_int_t(out_deg_o, &out_deg)) return NULL; - if (is_directed && igraphmodule_PyObject_to_vector_t(in_deg_o, &in_deg, 0)) { - igraph_vector_destroy(&out_deg); + if (is_directed && igraphmodule_PyObject_to_vector_int_t(in_deg_o, &in_deg)) { + igraph_vector_int_destroy(&out_deg); return NULL; } @@ -521,16 +521,16 @@ PyObject* igraphmodule_is_graphical(PyObject *self, PyObject *args, PyObject *kw if (igraph_is_graphical(&out_deg, is_directed ? &in_deg : 0, allowed_edge_types, &result)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&out_deg); + igraph_vector_int_destroy(&out_deg); if (is_directed) { - igraph_vector_destroy(&in_deg); + igraph_vector_int_destroy(&in_deg); } return NULL; } - igraph_vector_destroy(&out_deg); + igraph_vector_int_destroy(&out_deg); if (is_directed) { - igraph_vector_destroy(&in_deg); + igraph_vector_int_destroy(&in_deg); } if (result) From d096d47a0b36efc443c657e9bbdf1d1df5a3ed40 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 10 Aug 2021 18:18:49 +1000 Subject: [PATCH 0339/1681] A few more test files pass --- src/_igraph/convert.c | 6 +-- src/_igraph/convert.h | 2 +- src/_igraph/graphobject.c | 102 +++++++++++++++++++------------------- tests/test_attributes.py | 8 +-- tests/test_cliques.py | 5 +- 5 files changed, 64 insertions(+), 59 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 97f91dc77..ae458f03b 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1350,16 +1350,16 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { /** * \ingroup python_interface_conversion - * \brief Converts an igraph \c igraph_vector_t to a Python integer tuple + * \brief Converts an igraph \c igraph_vector_int_t to a Python integer tuple * * \param v the \c igraph_vector_t containing the vector to be converted * \return the Python integer tuple as a \c PyObject*, or \c NULL if an error occurred */ -PyObject* igraphmodule_vector_t_to_PyTuple(const igraph_vector_t *v) { +PyObject* igraphmodule_vector_int_t_to_PyTuple(const igraph_vector_int_t *v) { PyObject* tuple; Py_ssize_t n, i; - n=igraph_vector_size(v); + n=igraph_vector_int_size(v); if (n<0) return igraphmodule_handle_igraph_error(); tuple=PyTuple_New(n); diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index e0edcb29c..9f8de757d 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -143,7 +143,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v); PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, igraphmodule_conv_t type); -PyObject* igraphmodule_vector_t_to_PyTuple(const igraph_vector_t *v); +PyObject* igraphmodule_vector_int_t_to_PyTuple(const igraph_vector_int_t *v); PyObject* igraphmodule_vector_int_t_pair_to_PyList(const igraph_vector_int_t *v1, const igraph_vector_int_t *v2); PyObject* igraphmodule_vector_int_t_to_PyList_pairs(const igraph_vector_int_t *v); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 3fb559fcf..aa2433286 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -723,7 +723,7 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, PyObject *dtype_o = Py_None; PyObject *dmode_o = Py_None; igraph_neimode_t dmode = IGRAPH_ALL; - igraph_vector_t result; + igraph_vector_int_t result; igraph_vs_t vs; igraph_bool_t return_single = 0; @@ -745,7 +745,7 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, return NULL; } - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&result, 0)) { igraph_vs_destroy(&vs); return NULL; } @@ -754,16 +754,16 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, dmode, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } if (!return_single) - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); + list = igraphmodule_vector_int_t_to_PyList(&result); else list = PyLong_FromLong((long int)VECTOR(result)[0]); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); igraph_vs_destroy(&vs); return list; @@ -4043,7 +4043,7 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel PyObject *types_o = Py_None, *multiplicity_o = Py_True, *mul1 = 0, *mul2 = 0; igraphmodule_GraphObject *result1 = 0, *result2 = 0; igraph_vector_bool_t* types = 0; - igraph_vector_t multiplicities[2]; + igraph_vector_int_t multiplicities[2]; igraph_t g1, g2; igraph_t *p_g1 = &g1, *p_g2 = &g2; long int probe1 = -1; @@ -4065,14 +4065,14 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel } if (PyObject_IsTrue(multiplicity_o)) { - if (igraph_vector_init(&multiplicities[0], 0)) { + if (igraph_vector_int_init(&multiplicities[0], 0)) { if (types) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_vector_init(&multiplicities[1], 0)) { - igraph_vector_destroy(&multiplicities[0]); + if (igraph_vector_int_init(&multiplicities[1], 0)) { + igraph_vector_int_destroy(&multiplicities[0]); if (types) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; @@ -4082,8 +4082,8 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel p_g1 ? &multiplicities[0] : 0, p_g2 ? &multiplicities[1] : 0, (igraph_integer_t) probe1)) { - igraph_vector_destroy(&multiplicities[0]); - igraph_vector_destroy(&multiplicities[1]); + igraph_vector_int_destroy(&multiplicities[0]); + igraph_vector_int_destroy(&multiplicities[1]); if (types) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; @@ -4108,23 +4108,23 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel if (PyObject_IsTrue(multiplicity_o)) { if (p_g1) { - mul1 = igraphmodule_vector_t_to_PyList(&multiplicities[0], IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&multiplicities[0]); + mul1 = igraphmodule_vector_int_t_to_PyList(&multiplicities[0]); + igraph_vector_int_destroy(&multiplicities[0]); if (mul1 == NULL) { - igraph_vector_destroy(&multiplicities[1]); + igraph_vector_int_destroy(&multiplicities[1]); return NULL; } } else { - igraph_vector_destroy(&multiplicities[0]); + igraph_vector_int_destroy(&multiplicities[0]); } if (p_g2) { - mul2 = igraphmodule_vector_t_to_PyList(&multiplicities[1], IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&multiplicities[1]); + mul2 = igraphmodule_vector_int_t_to_PyList(&multiplicities[1]); + igraph_vector_int_destroy(&multiplicities[1]); if (mul2 == NULL) return NULL; } else { - igraph_vector_destroy(&multiplicities[1]); + igraph_vector_int_destroy(&multiplicities[1]); } if (p_g1 && p_g2) { @@ -4506,7 +4506,7 @@ PyObject *igraphmodule_Graph_contract_vertices(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char* kwlist[] = {"mapping", "combine_attrs", NULL }; PyObject *mapping_o, *combination_o = Py_None; - igraph_vector_t mapping; + igraph_vector_int_t mapping; igraph_attribute_combination_t combination; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &mapping_o, @@ -4517,19 +4517,19 @@ PyObject *igraphmodule_Graph_contract_vertices(igraphmodule_GraphObject * self, combination_o, &combination)) return NULL; - if (igraphmodule_PyObject_to_vector_t(mapping_o, &mapping, 1)) { + if (igraphmodule_PyObject_to_vector_int_t(mapping_o, &mapping)) { igraph_attribute_combination_destroy(&combination); return NULL; } if (igraph_contract_vertices(&self->g, &mapping, &combination)) { igraph_attribute_combination_destroy(&combination); - igraph_vector_destroy(&mapping); + igraph_vector_int_destroy(&mapping); return NULL; } igraph_attribute_combination_destroy(&combination); - igraph_vector_destroy(&mapping); + igraph_vector_int_destroy(&mapping); Py_RETURN_NONE; } @@ -6550,20 +6550,20 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s * \sa igraph_triad_census */ PyObject *igraphmodule_Graph_triad_census(igraphmodule_GraphObject *self) { - igraph_vector_t result; + igraph_vector_int_t result; PyObject *list; - if (igraph_vector_init(&result, 16)) { + if (igraph_vector_int_init(&result, 16)) { return igraphmodule_handle_igraph_error(); } if (igraph_triad_census(&self->g, &result)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyTuple(&result); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyTuple(&result); + igraph_vector_int_destroy(&result); return list; } @@ -7737,9 +7737,9 @@ PyObject *igraphmodule_Graph_get_incidence(igraphmodule_GraphObject * self, matrix_o = igraphmodule_matrix_t_to_PyList(&matrix, IGRAPHMODULE_TYPE_INT); igraph_matrix_destroy(&matrix); - row_ids_o = igraphmodule_vector_t_to_PyList(&row_ids, IGRAPHMODULE_TYPE_INT); + row_ids_o = igraphmodule_vector_int_t_to_PyList(&row_ids); igraph_vector_int_destroy(&row_ids); - col_ids_o = igraphmodule_vector_t_to_PyList(&col_ids, IGRAPHMODULE_TYPE_INT); + col_ids_o = igraphmodule_vector_int_t_to_PyList(&col_ids); igraph_vector_int_destroy(&col_ids); return Py_BuildValue("NNN", matrix_o, row_ids_o, col_ids_o); @@ -10908,8 +10908,8 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, return NULL; for (i = 0; i < n; i++) { - igraph_vector_t *vec = (igraph_vector_t *) VECTOR(result)[i]; - item = igraphmodule_vector_t_to_PyTuple(vec); + igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; + item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); @@ -10952,11 +10952,11 @@ PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject * self) return NULL; for (i = 0; i < n; i++) { - igraph_vector_t *vec = (igraph_vector_t *) VECTOR(result)[i]; - item = igraphmodule_vector_t_to_PyTuple(vec); + igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; + item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); + igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; @@ -10964,7 +10964,7 @@ PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject * self) else { PyList_SET_ITEM(list, i, item); } - igraph_vector_destroy(vec); + igraph_vector_int_destroy(vec); } igraph_vector_ptr_destroy_all(&result); @@ -11058,11 +11058,11 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, return NULL; for (i = 0; i < n; i++) { - igraph_vector_t *vec = (igraph_vector_t *) VECTOR(result)[i]; - item = igraphmodule_vector_t_to_PyTuple(vec); + igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; + item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); + igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; @@ -11070,7 +11070,7 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, else { PyList_SET_ITEM(list, i, item); } - igraph_vector_destroy(vec); + igraph_vector_int_destroy(vec); } igraph_vector_ptr_destroy_all(&result); @@ -11138,11 +11138,11 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject return NULL; for (i = 0; i < n; i++) { - igraph_vector_t *vec = (igraph_vector_t *) VECTOR(result)[i]; - item = igraphmodule_vector_t_to_PyTuple(vec); + igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; + item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); + igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; @@ -11150,7 +11150,7 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject else { PyList_SET_ITEM(list, i, item); } - igraph_vector_destroy(vec); + igraph_vector_int_destroy(vec); } igraph_vector_ptr_destroy_all(&result); @@ -11184,11 +11184,11 @@ PyObject return NULL; for (i = 0; i < n; i++) { - igraph_vector_t *vec = (igraph_vector_t *) VECTOR(result)[i]; - item = igraphmodule_vector_t_to_PyTuple(vec); + igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; + item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); + igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; @@ -11196,7 +11196,7 @@ PyObject else { PyList_SET_ITEM(list, i, item); } - igraph_vector_destroy(vec); + igraph_vector_int_destroy(vec); } igraph_vector_ptr_destroy_all(&result); @@ -11230,11 +11230,11 @@ PyObject return NULL; for (i = 0; i < n; i++) { - igraph_vector_t *vec = (igraph_vector_t *) VECTOR(result)[i]; - item = igraphmodule_vector_t_to_PyTuple(vec); + igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; + item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); + igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; @@ -11242,7 +11242,7 @@ PyObject else { PyList_SET_ITEM(list, i, item); } - igraph_vector_destroy(vec); + igraph_vector_int_destroy(vec); } igraph_vector_ptr_destroy_all(&result); diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 4e2522f56..1f77a33be 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -271,9 +271,11 @@ def suite(): attribute_suite = unittest.makeSuite(AttributeTests) attribute_combination_suite = unittest.makeSuite(AttributeCombinationTests) unicode_attributes_suite = unittest.makeSuite(UnicodeAttributeTests) - return unittest.TestSuite( - [attribute_suite, attribute_combination_suite, unicode_attributes_suite] - ) + return unittest.TestSuite([ + attribute_suite, + attribute_combination_suite, + unicode_attributes_suite, + ]) def test(): diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 0ef0c69ab..c124f5332 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -2,7 +2,10 @@ from igraph import * -from .utils import temporary_file +try: + from .utils import temporary_file +except ImportError: + from utils import temporary_file class CliqueTests(unittest.TestCase): From eed1b2439a591ae7cf640c87e21ec27b8b05865a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 10 Aug 2021 18:53:38 +1000 Subject: [PATCH 0340/1681] Start fixing community detection algorithms --- src/_igraph/convert.c | 60 +++++++++---------- src/_igraph/graphobject.c | 118 ++++++++++++++++++++------------------ 2 files changed, 92 insertions(+), 86 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index ae458f03b..5ad48d8b3 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1908,36 +1908,35 @@ PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, // create a new Python list list=PyList_New(nr); // populate the list with data - for (i=0; ig, &blocks, &cohesion, &parents, 0)) { igraph_vector_ptr_destroy(&blocks); - igraph_vector_destroy(&cohesion); - igraph_vector_destroy(&parents); + igraph_vector_int_destroy(&cohesion); + igraph_vector_int_destroy(&parents); igraphmodule_handle_igraph_error(); return NULL; } @@ -10839,21 +10839,21 @@ PyObject *igraphmodule_Graph_cohesive_blocks(igraphmodule_GraphObject *self, IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&blocks, igraph_vector_destroy); igraph_vector_ptr_destroy_all(&blocks); if (blocks_o == NULL) { - igraph_vector_destroy(&parents); - igraph_vector_destroy(&cohesion); + igraph_vector_int_destroy(&parents); + igraph_vector_int_destroy(&cohesion); return NULL; } - cohesion_o = igraphmodule_vector_t_to_PyList(&cohesion, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&cohesion); + cohesion_o = igraphmodule_vector_int_t_to_PyList(&cohesion); + igraph_vector_int_destroy(&cohesion); if (cohesion_o == NULL) { Py_DECREF(blocks_o); - igraph_vector_destroy(&parents); + igraph_vector_int_destroy(&parents); return NULL; } - parents_o = igraphmodule_vector_t_to_PyList(&parents, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&parents); + parents_o = igraphmodule_vector_int_t_to_PyList(&parents); + igraph_vector_int_destroy(&parents); if (parents_o == NULL) { Py_DECREF(blocks_o); @@ -11433,7 +11433,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj static char *kwlist[] = { "n", "weights", "arpack_options", NULL }; long int n=-1; PyObject *cl, *res, *merges, *weights_obj = Py_None; - igraph_vector_t members; + igraph_vector_int_t membership; igraph_vector_t *weights = 0; igraph_matrix_t m; igraph_real_t q; @@ -11445,12 +11445,12 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj return NULL; } - if (igraph_vector_init(&members, 0)) + if (igraph_vector_int_init(&membership, 0)) return igraphmodule_handle_igraph_error(); if (igraph_matrix_init(&m, 0, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&members); + igraph_vector_int_destroy(&membership); return 0; } @@ -11462,15 +11462,15 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj if (igraphmodule_attrib_to_vector_t(weights_obj, self, &weights, ATTRIBUTE_TYPE_EDGE)) { igraph_matrix_destroy(&m); - igraph_vector_destroy(&members); + igraph_vector_int_destroy(&membership); return NULL; } arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_community_leading_eigenvector(&self->g, weights, &m, &members, (igraph_integer_t) n, + if (igraph_community_leading_eigenvector(&self->g, weights, &m, &membership, (igraph_integer_t) n, igraphmodule_ARPACKOptions_get(arpack_options), &q, 0, 0, 0, 0, 0, 0)){ igraph_matrix_destroy(&m); - igraph_vector_destroy(&members); + igraph_vector_int_destroy(&membership); if (weights != 0) { igraph_vector_destroy(weights); free(weights); } @@ -11481,8 +11481,8 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj igraph_vector_destroy(weights); free(weights); } - cl = igraphmodule_vector_t_to_PyList(&members, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&members); + cl = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); if (cl == 0) { igraph_matrix_destroy(&m); return 0; @@ -11561,7 +11561,7 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, unsigned int nb_trials = 10; igraph_vector_t *e_ws = 0, *v_ws = 0; - igraph_vector_t membership; + igraph_vector_int_t membership; PyObject *res = Py_False; igraph_real_t codelength; @@ -11571,18 +11571,18 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, return NULL; } - if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { + if (igraph_vector_int_init(&membership, igraph_vcount(&self->g))) { igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_t(e_weights, self, &e_ws, ATTRIBUTE_TYPE_EDGE)) { - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); return NULL; } if (igraphmodule_attrib_to_vector_t(v_weights, self, &v_ws, ATTRIBUTE_TYPE_VERTEX)){ - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); if (e_ws) { igraph_vector_destroy(e_ws); free(e_ws); @@ -11595,7 +11595,7 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, /*nb_trials=*/nb_trials, /*out*/ &membership, &codelength)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); if (e_ws) { igraph_vector_destroy(e_ws); free(e_ws); @@ -11617,8 +11617,8 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, free(v_ws); } - res = igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); + res = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); if (!res) return NULL; @@ -11636,7 +11636,8 @@ PyObject *igraphmodule_Graph_community_label_propagation( static char *kwlist[] = { "weights", "initial", "fixed", NULL }; PyObject *weights_o = Py_None, *initial_o = Py_None, *fixed_o = Py_None; PyObject *result; - igraph_vector_t membership, *ws = 0, *initial = 0; + igraph_vector_int_t membership; + igraph_vector_t *ws = 0, *initial = 0; igraph_vector_bool_t fixed; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &weights_o, &initial_o, &fixed_o)) { @@ -11659,13 +11660,13 @@ PyObject *igraphmodule_Graph_community_label_propagation( return NULL; } - igraph_vector_init(&membership, igraph_vcount(&self->g)); + igraph_vector_int_init(&membership, igraph_vcount(&self->g)); if (igraph_community_label_propagation(&self->g, &membership, ws, initial, (fixed_o != Py_None ? &fixed : 0), 0)) { if (fixed_o != Py_None) igraph_vector_bool_destroy(&fixed); if (ws) { igraph_vector_destroy(ws); free(ws); } if (initial) { igraph_vector_destroy(initial); free(initial); } - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); return igraphmodule_handle_igraph_error(); } @@ -11673,8 +11674,8 @@ PyObject *igraphmodule_Graph_community_label_propagation( if (ws) { igraph_vector_destroy(ws); free(ws); } if (initial) { igraph_vector_destroy(initial); free(initial); } - result=igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); + result=igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); return result; } @@ -11689,7 +11690,8 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self PyObject *return_levels = Py_False; PyObject *mss, *qs, *res, *weights = Py_None; igraph_matrix_t memberships; - igraph_vector_t membership, modularity; + igraph_vector_int_t membership; + igraph_vector_t modularity; double resolution = 1; igraph_vector_t *ws; @@ -11701,13 +11703,13 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self return NULL; igraph_matrix_init(&memberships, 0, 0); - igraph_vector_init(&membership, 0); + igraph_vector_int_init(&membership, 0); igraph_vector_init(&modularity, 0); if (igraph_community_multilevel(&self->g, ws, resolution, &membership, &memberships, &modularity)) { if (ws) { igraph_vector_destroy(ws); free(ws); } - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); igraph_vector_destroy(&modularity); igraph_matrix_destroy(&memberships); return igraphmodule_handle_igraph_error(); @@ -11718,7 +11720,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self qs=igraphmodule_vector_t_to_PyList(&modularity, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&modularity); if (!qs) { - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); igraph_matrix_destroy(&memberships); return NULL; } @@ -11731,10 +11733,10 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self res=Py_BuildValue("NN", mss, qs); /* steals references */ } } else { - res=igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); + res=igraphmodule_vector_int_t_to_PyList(&membership); } - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); igraph_matrix_destroy(&memberships); return res; @@ -11748,7 +11750,7 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( static char *kwlist[] = {"weights", NULL}; PyObject *weights_o = Py_None; igraph_real_t modularity; - igraph_vector_t membership; + igraph_vector_int_t membership; igraph_vector_t* weights = 0; PyObject *res; @@ -11756,21 +11758,21 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( &weights_o)) return NULL; - if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { + if (igraph_vector_int_init(&membership, igraph_vcount(&self->g))) { igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); return NULL; } if (igraph_community_optimal_modularity(&self->g, &modularity, &membership, weights)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); if (weights != 0) { igraph_vector_destroy(weights); free(weights); } @@ -11781,8 +11783,8 @@ PyObject *igraphmodule_Graph_community_optimal_modularity( igraph_vector_destroy(weights); free(weights); } - res = igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); + res = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); if (!res) return NULL; @@ -11812,7 +11814,8 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, igraph_spincomm_update_t update_rule = IGRAPH_SPINCOMM_UPDATE_CONFIG; double gamma = 1; double lambda = 1; - igraph_vector_t *weights = 0, membership; + igraph_vector_t *weights = 0; + igraph_vector_int_t membership; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlOdddOdOd", kwlist, &weights_o, &spins, &parupdate_o, &start_temp, &stop_temp, @@ -11827,14 +11830,14 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, return NULL; } - if (igraph_vector_init(&membership, igraph_vcount(&self->g))) { + if (igraph_vector_int_init(&membership, igraph_vcount(&self->g))) { igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); return NULL; } @@ -11844,7 +11847,7 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, start_temp, stop_temp, cool_fact, update_rule, gamma, impl, lambda)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); if (weights != 0) { igraph_vector_destroy(weights); free(weights); @@ -11857,8 +11860,8 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, free(weights); } - res = igraphmodule_vector_t_to_PyList(&membership, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&membership); + res = igraphmodule_vector_int_t_to_PyList(&membership); + igraph_vector_int_destroy(&membership); return res; } @@ -11934,7 +11937,8 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, long int n_iterations = 2; double resolution_parameter = 1.0; double beta = 0.01; - igraph_vector_t *edge_weights = NULL, *node_weights = NULL, *membership = NULL; + igraph_vector_t *edge_weights = NULL, *node_weights = NULL; + igraph_vector_int_t *membership = NULL; igraph_bool_t start = 1; igraph_bool_t normalize_resolution = 0; igraph_integer_t nb_clusters = 0; @@ -11959,7 +11963,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, } /* Get initial membership */ - if (!error && igraphmodule_attrib_to_vector_t(initial_membership_o, self, &membership, + if (!error && igraphmodule_attrib_to_vector_int_t(initial_membership_o, self, &membership, ATTRIBUTE_TYPE_VERTEX)) { igraphmodule_handle_igraph_error(); error = -1; @@ -11967,12 +11971,12 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, if (!error && membership == 0) { start = 0; - membership = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); + membership = (igraph_vector_int_t*)calloc(1, sizeof(igraph_vector_int_t)); if (membership==0) { PyErr_NoMemory(); error = -1; } else { - igraph_vector_init(membership, 0); + igraph_vector_int_init(membership, 0); } } @@ -12031,11 +12035,11 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, } if (!error && membership != 0) { - res = igraphmodule_vector_t_to_PyList(membership, IGRAPHMODULE_TYPE_INT); + res = igraphmodule_vector_int_t_to_PyList(membership); } if (membership != 0) { - igraph_vector_destroy(membership); + igraph_vector_int_destroy(membership); free(membership); } From 33e0b4bb57dd87b1e94b5494fe0f91c2abdbe6c7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 11 Aug 2021 10:19:17 +1000 Subject: [PATCH 0341/1681] Some matrix conversions, passes test_edgeseq.py --- src/_igraph/convert.c | 73 +++++++++++++++++++++++++++++++++++ src/_igraph/convert.h | 2 + src/_igraph/graphobject.c | 16 ++++---- src/_igraph/igraphmodule.c | 38 +++++++++--------- src/_igraph/indexing.c | 58 ++++++++++++++-------------- src/_igraph/vertexseqobject.c | 2 +- tests/test_edgeseq.py | 5 ++- 7 files changed, 136 insertions(+), 58 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 5ad48d8b3..7afccb12d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2044,6 +2044,79 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count(PyObject *o, igrap return 0; } +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_matrix_int_t + * + * \param o the Python object representing the list of lists + * \param m the address of an uninitialized \c igraph_matrix_int_t + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyList_to_matrix_int_t(PyObject* o, igraph_matrix_int_t *m) { + return igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(o, m, 0); +} + +/** + * \ingroup python_interface_conversion + * \brief Converts a Python list of lists to an \c igraph_matrix_int_t, ensuring + * that the matrix has at least the given number of columns + * + * \param o the Python object representing the list of lists + * \param m the address of an uninitialized \c igraph_matrix_int_t + * \param num_cols the minimum number of columns in the matrix + * \return 0 if everything was OK, 1 otherwise. Sets appropriate exceptions. + */ +int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, igraph_matrix_int_t *m, int min_cols) { + Py_ssize_t nr, nc, n, i, j; + PyObject *row, *item; + int was_warned = 0; + + /* calculate the matrix dimensions */ + if (!PySequence_Check(o) || PyUnicode_Check(o)) { + PyErr_SetString(PyExc_TypeError, "matrix expected (list of sequences)"); + return 1; + } + + nr = PySequence_Size(o); + nc = min_cols > 0 ? min_cols : 0; + for (i = 0; i < nr; i++) { + row = PySequence_GetItem(o, i); + if (!PySequence_Check(row)) { + Py_DECREF(row); + PyErr_SetString(PyExc_TypeError, "matrix expected (list of sequences)"); + return 1; + } + n = PySequence_Size(row); + Py_DECREF(row); + if (n > nc) { + nc = n; + } + } + + igraph_matrix_int_init(m, nr, nc); + for (i = 0; i < nr; i++) { + row = PySequence_GetItem(o, i); + n = PySequence_Size(row); + for (j = 0; j < n; j++) { + item = PySequence_GetItem(row, j); + if (PyLong_Check(item)) { + MATRIX(*m, i, j) = (igraph_integer_t)PyLong_AsLong(item); + } else if (PyLong_Check(item)) { + MATRIX(*m, i, j) = (igraph_integer_t)PyLong_AsLong(item); + } else if (PyFloat_Check(item)) { + MATRIX(*m, i, j) = (igraph_integer_t)PyFloat_AsDouble(item); + } else if (!was_warned) { + PyErr_Warn(PyExc_Warning, "non-numeric value in matrix ignored"); + was_warned=1; + } + Py_DECREF(item); + } + Py_DECREF(row); + } + + return 0; +} + /** * \ingroup python_interface_conversion * \brief Converts a Python list of lists to an \c igraph_vector_ptr_t diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 9f8de757d..28a3ae14d 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -102,6 +102,8 @@ int igraphmodule_PyObject_to_edgelist( int igraphmodule_PyList_to_matrix_t(PyObject *o, igraph_matrix_t *m); int igraphmodule_PyList_to_matrix_t_with_minimum_column_count(PyObject *o, igraph_matrix_t *m, int min_cols); +int igraphmodule_PyList_to_matrix_int_t(PyObject *o, igraph_matrix_int_t *m); +int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, igraph_matrix_int_t *m, int min_cols); PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v); int igraphmodule_PyList_to_strvector_t(PyObject* v, igraph_strvector_t *result); int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d0fe304ba..439874876 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1953,7 +1953,7 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, PyObject * args, PyObject * kwds) { igraphmodule_GraphObject *self; igraph_t g; - igraph_matrix_t m; + igraph_matrix_int_t m; PyObject *matrix, *mode_o = Py_None; igraph_adjacency_t mode = IGRAPH_ADJ_DIRECTED; @@ -1964,7 +1964,7 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, return NULL; if (igraphmodule_PyObject_to_adjacency_t(mode_o, &mode)) return NULL; - if (igraphmodule_PyList_to_matrix_t(matrix, &m)) { + if (igraphmodule_PyList_to_matrix_int_t(matrix, &m)) { PyErr_SetString(PyExc_TypeError, "Error while converting adjacency matrix"); return NULL; @@ -1972,11 +1972,11 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, if (igraph_adjacency(&g, &m, mode)) { igraphmodule_handle_igraph_error(); - igraph_matrix_destroy(&m); + igraph_matrix_int_destroy(&m); return NULL; } - igraph_matrix_destroy(&m); + igraph_matrix_int_destroy(&m); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2763,7 +2763,7 @@ PyObject *igraphmodule_Graph_K_Regular(PyTypeObject * type, PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, PyObject * args, PyObject * kwds) { - igraph_vector_t dimvector; + igraph_vector_int_t dimvector; long int nei = 1; igraph_bool_t directed; igraph_bool_t mutual; @@ -2784,17 +2784,17 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, mutual = PyObject_IsTrue(o_mutual); circular = PyObject_IsTrue(o_circular); - if (igraphmodule_PyObject_to_vector_t(o_dimvector, &dimvector, 1)) + if (igraphmodule_PyObject_to_vector_int_t(o_dimvector, &dimvector)) return NULL; if (igraph_lattice(&g, &dimvector, (igraph_integer_t) nei, directed, mutual, circular)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&dimvector); + igraph_vector_int_destroy(&dimvector); return NULL; } - igraph_vector_destroy(&dimvector); + igraph_vector_int_destroy(&dimvector); CREATE_GRAPH_FROM_TYPE(self, g, type); diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index d19883e0c..d26a62229 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -231,7 +231,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd static char* kwlist[] = {"vs", "coords", NULL}; PyObject *vs, *o, *o1=0, *o2=0, *coords = Py_False; igraph_matrix_t mtrx; - igraph_vector_t result; + igraph_vector_int_t result; igraph_matrix_t resmat; long no_of_nodes, i; @@ -290,7 +290,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd } if (!PyObject_IsTrue(coords)) { - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&result, 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); return NULL; @@ -298,11 +298,11 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd if (igraph_convex_hull(&mtrx, &result, 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&mtrx); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - o=igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + o=igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); } else { if (igraph_matrix_init(&resmat, 0, 0)) { igraphmodule_handle_igraph_error(); @@ -329,42 +329,42 @@ PyObject* igraphmodule_community_to_membership(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "merges", "nodes", "steps", "return_csize", NULL }; PyObject *merges_o, *return_csize = Py_False, *result_o; - igraph_matrix_t merges; - igraph_vector_t result, csize, *csize_p = 0; + igraph_matrix_int_t merges; + igraph_vector_int_t result, csize, *csize_p = 0; long int nodes, steps; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!ll|O", kwlist, &PyList_Type, &merges_o, &nodes, &steps, &return_csize)) return NULL; - if (igraphmodule_PyList_to_matrix_t_with_minimum_column_count(merges_o, &merges, 2)) return NULL; + if (igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2)) return NULL; - if (igraph_vector_init(&result, nodes)) { + if (igraph_vector_int_init(&result, nodes)) { igraphmodule_handle_igraph_error(); - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); return NULL; } if (PyObject_IsTrue(return_csize)) { - igraph_vector_init(&csize, 0); + igraph_vector_int_init(&csize, 0); csize_p = &csize; } if (igraph_community_to_membership(&merges, (igraph_integer_t)nodes, (igraph_integer_t)steps, &result, csize_p)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); - if (csize_p) igraph_vector_destroy(csize_p); - igraph_matrix_destroy(&merges); + igraph_vector_int_destroy(&result); + if (csize_p) igraph_vector_int_destroy(csize_p); + igraph_matrix_int_destroy(&merges); return NULL; } - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); - result_o = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + result_o = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); if (csize_p) { - PyObject* csize_o = igraphmodule_vector_t_to_PyList(csize_p, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(csize_p); + PyObject* csize_o = igraphmodule_vector_int_t_to_PyList(csize_p); + igraph_vector_int_destroy(csize_p); if (csize_o) return Py_BuildValue("NN", result_o, csize_o); Py_DECREF(result_o); return NULL; diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index 8eececb9c..08cd04d95 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -133,7 +133,7 @@ PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values) { - igraph_vector_t eids; + igraph_vector_int_t eids; igraph_integer_t eid; igraph_vit_t vit; PyObject *result = 0, *item; @@ -142,11 +142,11 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, if (igraph_vs_is_all(to)) { /* Simple case: all edges */ - IGRAPH_PYCHECK(igraph_vector_init(&eids, 0)); - IGRAPH_FINALLY(igraph_vector_destroy, &eids); + IGRAPH_PYCHECK(igraph_vector_int_init(&eids, 0)); + IGRAPH_FINALLY(igraph_vector_int_destroy, &eids); IGRAPH_PYCHECK(igraph_incident(graph, &eids, from, neimode)); - n = igraph_vector_size(&eids); + n = igraph_vector_int_size(&eids); result = igraphmodule_PyList_Zeroes(igraph_vcount(graph)); if (result == 0) { IGRAPH_FINALLY_FREE(); @@ -165,7 +165,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, } IGRAPH_FINALLY_CLEAN(1); - igraph_vector_destroy(&eids); + igraph_vector_int_destroy(&eids); return result; } @@ -228,28 +228,28 @@ static INLINE igraph_bool_t deleting_edge(PyObject* value) { * adjacency matrix assignment. */ typedef struct { - igraph_vector_t to_add; + igraph_vector_int_t to_add; PyObject* to_add_values; - igraph_vector_t to_delete; + igraph_vector_int_t to_delete; } igraphmodule_i_Graph_adjmatrix_set_index_data_t; int igraphmodule_i_Graph_adjmatrix_set_index_data_init( igraphmodule_i_Graph_adjmatrix_set_index_data_t* data) { - if (igraph_vector_init(&data->to_add, 0)) { + if (igraph_vector_int_init(&data->to_add, 0)) { igraphmodule_handle_igraph_error(); return -1; } - if (igraph_vector_init(&data->to_delete, 0)) { + if (igraph_vector_int_init(&data->to_delete, 0)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&data->to_delete); + igraph_vector_int_destroy(&data->to_delete); return -1; } data->to_add_values = PyList_New(0); if (data->to_add_values == 0) { - igraph_vector_destroy(&data->to_add); - igraph_vector_destroy(&data->to_delete); + igraph_vector_int_destroy(&data->to_add); + igraph_vector_int_destroy(&data->to_delete); return -1; } @@ -258,8 +258,8 @@ int igraphmodule_i_Graph_adjmatrix_set_index_data_init( void igraphmodule_i_Graph_adjmatrix_set_index_data_destroy( igraphmodule_i_Graph_adjmatrix_set_index_data_t* data) { - igraph_vector_destroy(&data->to_add); - igraph_vector_destroy(&data->to_delete); + igraph_vector_int_destroy(&data->to_add); + igraph_vector_int_destroy(&data->to_delete); Py_DECREF(data->to_add_values); } @@ -311,9 +311,9 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, if (deleting_edge(item)) { /* Deleting edges if eid != -1 */ if (eid != -1) { - if (igraph_vector_push_back(&data->to_delete, eid)) { + if (igraph_vector_int_push_back(&data->to_delete, eid)) { igraphmodule_handle_igraph_error(); - igraph_vector_clear(&data->to_delete); + igraph_vector_int_clear(&data->to_delete); ok = 0; break; } @@ -321,10 +321,10 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, } else { if (eid == -1) { /* Adding edges */ - if (igraph_vector_push_back(&data->to_add, v1) || - igraph_vector_push_back(&data->to_add, v2)) { + if (igraph_vector_int_push_back(&data->to_add, v1) || + igraph_vector_int_push_back(&data->to_add, v2)) { igraphmodule_handle_igraph_error(); - igraph_vector_clear(&data->to_add); + igraph_vector_int_clear(&data->to_add); ok = 0; break; } @@ -332,7 +332,7 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, Py_INCREF(new_value); if (PyList_Append(data->to_add_values, new_value)) { Py_DECREF(new_value); - igraph_vector_clear(&data->to_add); + igraph_vector_int_clear(&data->to_add); ok = 0; break; } @@ -342,7 +342,7 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, Py_INCREF(item); if (PyList_SetItem(values, eid, item)) { Py_DECREF(item); - igraph_vector_clear(&data->to_add); + igraph_vector_int_clear(&data->to_add); } } } @@ -372,9 +372,9 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, if (deleting) { /* Deleting edges if eid != -1 */ if (eid != -1) { - if (igraph_vector_push_back(&data->to_delete, eid)) { + if (igraph_vector_int_push_back(&data->to_delete, eid)) { igraphmodule_handle_igraph_error(); - igraph_vector_clear(&data->to_delete); + igraph_vector_int_clear(&data->to_delete); ok = 0; break; } @@ -382,10 +382,10 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, } else { if (eid == -1) { /* Adding edges */ - if (igraph_vector_push_back(&data->to_add, v1) || - igraph_vector_push_back(&data->to_add, v2)) { + if (igraph_vector_int_push_back(&data->to_add, v1) || + igraph_vector_int_push_back(&data->to_add, v2)) { igraphmodule_handle_igraph_error(); - igraph_vector_clear(&data->to_add); + igraph_vector_int_clear(&data->to_add); ok = 0; break; } @@ -393,7 +393,7 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, Py_INCREF(new_value); if (PyList_Append(data->to_add_values, new_value)) { Py_DECREF(new_value); - igraph_vector_clear(&data->to_add); + igraph_vector_int_clear(&data->to_add); ok = 0; break; } @@ -403,7 +403,7 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, Py_INCREF(new_value); if (PyList_SetItem(values, eid, new_value)) { Py_DECREF(new_value); - igraph_vector_clear(&data->to_add); + igraph_vector_int_clear(&data->to_add); } } } @@ -517,7 +517,7 @@ int igraphmodule_Graph_adjmatrix_set_index(igraph_t* graph, if (ok) { /* Third phase: add the new edges in one batch */ - if (!igraph_vector_empty(&data.to_add)) { + if (!igraph_vector_int_empty(&data.to_add)) { eid = igraph_ecount(graph); igraph_add_edges(graph, &data.to_add, 0); if (values != 0) { diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 7ee4fcde4..4b2e83a3b 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -75,7 +75,7 @@ igraphmodule_VertexSeq_copy(igraphmodule_VertexSeqObject* o) { if (igraph_vs_type(&o->vs) == IGRAPH_VS_VECTOR) { igraph_vector_int_t v; - if (igraph_vector_copy(&v, o->vs.data.vecptr)) { + if (igraph_vector_int_copy(&v, o->vs.data.vecptr)) { igraphmodule_handle_igraph_error(); return 0; } diff --git a/tests/test_edgeseq.py b/tests/test_edgeseq.py index 74667b758..f1b451512 100644 --- a/tests/test_edgeseq.py +++ b/tests/test_edgeseq.py @@ -4,7 +4,10 @@ from igraph import * -from .utils import is_pypy +try: + from .utils import is_pypy +except ImportError: + from utils import is_pypy try: import numpy as np From afbcb5d790bf7ce11e307e9221bdb272474aae22 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 11 Aug 2021 11:24:17 +1000 Subject: [PATCH 0342/1681] Pass test_games.py and test_foreign.py --- src/_igraph/convert.c | 45 ++++++++ src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 214 +++++++++++++++++++------------------ src/_igraph/igraphmodule.c | 32 +++--- tests/test_foreign.py | 25 ++--- tests/test_games.py | 9 +- 6 files changed, 189 insertions(+), 137 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 7afccb12d..7f3e626e6 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1939,6 +1939,51 @@ PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, return list; } +/** + * \ingroup python_interface_conversion + * \brief Converts an igraph \c igraph_matrix_int_t to a Python list of lists + * + * \param m the \c igraph_matrix_int_t containing the matrix to be converted + * \return the Python list of lists as a \c PyObject*, or \c NULL if an error occurred + */ +PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m) { + PyObject *list, *row, *item; + Py_ssize_t nr, nc, i, j; + + nr = igraph_matrix_int_nrow(m); + nc = igraph_matrix_int_ncol(m); + if (nr<0 || nc<0) + return igraphmodule_handle_igraph_error(); + + // create a new Python list + list=PyList_New(nr); + // populate the list with data + for (i=0; ig)[ATTRHASH_IDX_VERTEX], attribute_key, type_vec_o) == -1) { Py_DECREF(type_vec_o); + igraph_vector_int_destroy(&in_type_vec); + igraph_vector_int_destroy(&out_type_vec); igraph_matrix_destroy(&pm); igraph_matrix_destroy(&td); - igraph_vector_destroy(&in_type_vec); - igraph_vector_destroy(&out_type_vec); Py_DECREF(self); return NULL; } } Py_DECREF(type_vec_o); - igraph_vector_destroy(&in_type_vec); - igraph_vector_destroy(&out_type_vec); + igraph_vector_int_destroy(&in_type_vec); + igraph_vector_int_destroy(&out_type_vec); } igraph_matrix_destroy(&pm); @@ -4372,7 +4372,7 @@ PyObject *igraphmodule_Graph_clusters(igraphmodule_GraphObject * self, { static char *kwlist[] = { "mode", NULL }; igraph_connectedness_t mode = IGRAPH_STRONG; - igraph_vector_t res1, res2; + igraph_vector_int_t res1, res2; igraph_integer_t no; PyObject *list, *mode_o = Py_None; @@ -4382,19 +4382,19 @@ PyObject *igraphmodule_Graph_clusters(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_connectedness_t(mode_o, &mode)) return NULL; - igraph_vector_init(&res1, igraph_vcount(&self->g)); - igraph_vector_init(&res2, 10); + igraph_vector_int_init(&res1, igraph_vcount(&self->g)); + igraph_vector_int_init(&res2, 10); if (igraph_clusters(&self->g, &res1, &res2, &no, mode)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&res1); - igraph_vector_destroy(&res2); + igraph_vector_int_destroy(&res1); + igraph_vector_int_destroy(&res2); return NULL; } - list = igraphmodule_vector_t_to_PyList(&res1, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res1); - igraph_vector_destroy(&res2); + list = igraphmodule_vector_int_t_to_PyList(&res1); + igraph_vector_int_destroy(&res1); + igraph_vector_int_destroy(&res2); return list; } @@ -4953,7 +4953,8 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * return NULL; } - res = (igraph_vector_t *) calloc(no_of_target_nodes, sizeof(igraph_vector_t)); + // FIXME: is this alright? + res = (igraph_vector_int_t *) calloc(no_of_target_nodes, sizeof(igraph_vector_int_t)); if (!res) { PyErr_SetString(PyExc_MemoryError, ""); igraph_vector_ptr_destroy(ptrvec); free(ptrvec); @@ -4964,13 +4965,13 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * for (i = 0; i < no_of_target_nodes; i++) { VECTOR(*ptrvec)[i] = &res[i]; - igraph_vector_init(&res[i], 0); + igraph_vector_int_init(&res[i], 0); } if (igraph_get_shortest_paths_dijkstra(&self->g, use_edges ? 0 : ptrvec, use_edges ? ptrvec : 0, from, to, weights, mode, 0, 0)) { igraphmodule_handle_igraph_error(); - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_destroy(&res[j]); + for (j = 0; j < no_of_target_nodes; j++) igraph_vector_int_destroy(&res[j]); free(res); igraph_vector_ptr_destroy(ptrvec); free(ptrvec); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4984,25 +4985,25 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * list = PyList_New(no_of_target_nodes); if (!list) { - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_destroy(&res[j]); + for (j = 0; j < no_of_target_nodes; j++) igraph_vector_int_destroy(&res[j]); free(res); return NULL; } for (i = 0; i < no_of_target_nodes; i++) { - item = igraphmodule_vector_t_to_PyList(&res[i], IGRAPHMODULE_TYPE_INT); + item = igraphmodule_vector_int_t_to_PyList(&res[i]); if (!item || PyList_SetItem(list, i, item)) { if (item) { Py_DECREF(item); } Py_DECREF(list); - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_destroy(&res[j]); + for (j = 0; j < no_of_target_nodes; j++) igraph_vector_int_destroy(&res[j]); free(res); return NULL; } } - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_destroy(&res[j]); + for (j = 0; j < no_of_target_nodes; j++) igraph_vector_int_destroy(&res[j]); free(res); return list; } @@ -10175,7 +10176,8 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, igraph_real_t result; long int vid1 = -1, vid2 = -1; igraph_integer_t v1, v2; - igraph_vector_t flow, cut, partition; + igraph_vector_t flow; + igraph_vector_int_t cut, partition; igraph_maxflow_stats_t stats; if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|O", kwlist, @@ -10194,16 +10196,16 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&cut, 0)) { + if (igraph_vector_int_init(&cut, 0)) { igraph_vector_destroy(&capacity_vector); igraph_vector_destroy(&flow); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&partition, 0)) { + if (igraph_vector_int_init(&partition, 0)) { igraph_vector_destroy(&capacity_vector); igraph_vector_destroy(&flow); - igraph_vector_destroy(&cut); + igraph_vector_int_destroy(&cut); return igraphmodule_handle_igraph_error(); } @@ -10211,8 +10213,8 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, v1, v2, &capacity_vector, &stats)) { igraph_vector_destroy(&capacity_vector); igraph_vector_destroy(&flow); - igraph_vector_destroy(&cut); - igraph_vector_destroy(&partition); + igraph_vector_int_destroy(&cut); + igraph_vector_int_destroy(&partition); return igraphmodule_handle_igraph_error(); } @@ -10222,21 +10224,21 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, igraph_vector_destroy(&flow); if (flow_o == NULL) { - igraph_vector_destroy(&cut); - igraph_vector_destroy(&partition); + igraph_vector_int_destroy(&cut); + igraph_vector_int_destroy(&partition); return NULL; } - cut_o = igraphmodule_vector_t_to_PyList(&cut, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&cut); + cut_o = igraphmodule_vector_int_t_to_PyList(&cut); + igraph_vector_int_destroy(&cut); if (cut_o == NULL) { - igraph_vector_destroy(&partition); + igraph_vector_int_destroy(&partition); return NULL; } - partition_o = igraphmodule_vector_t_to_PyList(&partition, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&partition); + partition_o = igraphmodule_vector_int_t_to_PyList(&partition); + igraph_vector_int_destroy(&partition); if (partition_o == NULL) return NULL; @@ -10455,7 +10457,7 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, int retval; igraph_vector_t capacity_vector; igraph_real_t value; - igraph_vector_t partition, partition2, cut; + igraph_vector_int_t partition, partition2, cut; igraph_integer_t source = -1, target = -1; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, @@ -10472,18 +10474,18 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, self, ATTRHASH_IDX_EDGE, 1.0)) return igraphmodule_handle_igraph_error(); - if (igraph_vector_init(&partition, 0)) { + if (igraph_vector_int_init(&partition, 0)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&partition2, 0)) { - igraph_vector_destroy(&partition); + if (igraph_vector_int_init(&partition2, 0)) { + igraph_vector_int_destroy(&partition); igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&cut, 0)) { - igraph_vector_destroy(&partition); - igraph_vector_destroy(&partition2); + if (igraph_vector_int_init(&cut, 0)) { + igraph_vector_int_destroy(&partition); + igraph_vector_int_destroy(&partition2); igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } @@ -10501,9 +10503,9 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, } if (retval) { - igraph_vector_destroy(&cut); - igraph_vector_destroy(&partition); - igraph_vector_destroy(&partition2); + igraph_vector_int_destroy(&cut); + igraph_vector_int_destroy(&partition); + igraph_vector_int_destroy(&partition2); igraph_vector_destroy(&capacity_vector); if (!PyErr_Occurred()) igraphmodule_handle_igraph_error(); @@ -10512,24 +10514,24 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, igraph_vector_destroy(&capacity_vector); - cut_o=igraphmodule_vector_t_to_PyList(&cut, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&cut); + cut_o=igraphmodule_vector_int_t_to_PyList(&cut); + igraph_vector_int_destroy(&cut); if (!cut_o) { - igraph_vector_destroy(&partition); - igraph_vector_destroy(&partition2); + igraph_vector_int_destroy(&partition); + igraph_vector_int_destroy(&partition2); return 0; } - part_o=igraphmodule_vector_t_to_PyList(&partition, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&partition); + part_o=igraphmodule_vector_int_t_to_PyList(&partition); + igraph_vector_int_destroy(&partition); if (!part_o) { Py_DECREF(cut_o); - igraph_vector_destroy(&partition2); + igraph_vector_int_destroy(&partition2); return 0; } - part2_o=igraphmodule_vector_t_to_PyList(&partition2, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&partition2); + part2_o=igraphmodule_vector_int_t_to_PyList(&partition2); + igraph_vector_int_destroy(&partition2); if (!part2_o) { Py_DECREF(part_o); Py_DECREF(cut_o); @@ -10603,7 +10605,7 @@ PyObject *igraphmodule_Graph_st_mincut(igraphmodule_GraphObject * self, PyObject *source_o, *target_o, *capacity_o = Py_None; igraph_vector_t capacity_vector; igraph_real_t value; - igraph_vector_t partition, partition2, cut; + igraph_vector_int_t partition, partition2, cut; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO", kwlist, &source_o, &target_o, &capacity_o)) @@ -10619,51 +10621,51 @@ PyObject *igraphmodule_Graph_st_mincut(igraphmodule_GraphObject * self, self, ATTRHASH_IDX_EDGE, 1.0)) return igraphmodule_handle_igraph_error(); - if (igraph_vector_init(&partition, 0)) { + if (igraph_vector_int_init(&partition, 0)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&partition2, 0)) { - igraph_vector_destroy(&partition); + if (igraph_vector_int_init(&partition2, 0)) { + igraph_vector_int_destroy(&partition); igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&cut, 0)) { - igraph_vector_destroy(&partition); - igraph_vector_destroy(&partition2); + if (igraph_vector_int_init(&cut, 0)) { + igraph_vector_int_destroy(&partition); + igraph_vector_int_destroy(&partition2); igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } if (igraph_st_mincut(&self->g, &value, &cut, &partition, &partition2, source, target, &capacity_vector)) { - igraph_vector_destroy(&cut); - igraph_vector_destroy(&partition); - igraph_vector_destroy(&partition2); + igraph_vector_int_destroy(&cut); + igraph_vector_int_destroy(&partition); + igraph_vector_int_destroy(&partition2); igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } igraph_vector_destroy(&capacity_vector); - cut_o=igraphmodule_vector_t_to_PyList(&cut, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&cut); + cut_o=igraphmodule_vector_int_t_to_PyList(&cut); + igraph_vector_int_destroy(&cut); if (!cut_o) { - igraph_vector_destroy(&partition); - igraph_vector_destroy(&partition2); + igraph_vector_int_destroy(&partition); + igraph_vector_int_destroy(&partition2); return NULL; } - part_o=igraphmodule_vector_t_to_PyList(&partition, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&partition); + part_o=igraphmodule_vector_int_t_to_PyList(&partition); + igraph_vector_int_destroy(&partition); if (!part_o) { Py_DECREF(cut_o); - igraph_vector_destroy(&partition2); + igraph_vector_int_destroy(&partition2); return NULL; } - part2_o=igraphmodule_vector_t_to_PyList(&partition2, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&partition2); + part2_o=igraphmodule_vector_int_t_to_PyList(&partition2); + igraph_vector_int_destroy(&partition2); if (!part2_o) { Py_DECREF(part_o); Py_DECREF(cut_o); @@ -11689,7 +11691,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self static char *kwlist[] = { "weights", "return_levels", "resolution", NULL }; PyObject *return_levels = Py_False; PyObject *mss, *qs, *res, *weights = Py_None; - igraph_matrix_t memberships; + igraph_matrix_int_t memberships; igraph_vector_int_t membership; igraph_vector_t modularity; double resolution = 1; @@ -11702,7 +11704,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_matrix_init(&memberships, 0, 0); + igraph_matrix_int_init(&memberships, 0, 0); igraph_vector_int_init(&membership, 0); igraph_vector_init(&modularity, 0); @@ -11711,7 +11713,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self if (ws) { igraph_vector_destroy(ws); free(ws); } igraph_vector_int_destroy(&membership); igraph_vector_destroy(&modularity); - igraph_matrix_destroy(&memberships); + igraph_matrix_int_destroy(&memberships); return igraphmodule_handle_igraph_error(); } @@ -11721,12 +11723,12 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self igraph_vector_destroy(&modularity); if (!qs) { igraph_vector_int_destroy(&membership); - igraph_matrix_destroy(&memberships); + igraph_matrix_int_destroy(&memberships); return NULL; } if (PyObject_IsTrue(return_levels)) { - mss=igraphmodule_matrix_t_to_PyList(&memberships, IGRAPHMODULE_TYPE_INT); + mss=igraphmodule_matrix_int_t_to_PyList(&memberships); if (!mss) { res = mss; } else { @@ -11737,7 +11739,7 @@ PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self } igraph_vector_int_destroy(&membership); - igraph_matrix_destroy(&memberships); + igraph_matrix_int_destroy(&memberships); return res; } @@ -11873,7 +11875,7 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "weights", "steps", NULL }; PyObject *ms, *qs, *res, *weights = Py_None; - igraph_matrix_t merges; + igraph_matrix_int_t merges; int steps=4; igraph_vector_t q, *ws=0; @@ -11884,7 +11886,7 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_matrix_init(&merges, 0, 0); + igraph_matrix_int_init(&merges, 0, 0); igraph_vector_init(&q, 0); if (igraph_community_walktrap(&self->g, ws, steps, &merges, &q, 0)) { @@ -11892,7 +11894,7 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, igraph_vector_destroy(ws); free(ws); } igraph_vector_destroy(&q); - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); return igraphmodule_handle_igraph_error(); } if (ws) { @@ -11902,12 +11904,12 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, qs = igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&q); if (!qs) { - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); return NULL; } - ms = igraphmodule_matrix_t_to_PyList(&merges, IGRAPHMODULE_TYPE_INT); - igraph_matrix_destroy(&merges); + ms = igraphmodule_matrix_int_t_to_PyList(&merges); + igraph_matrix_int_destroy(&merges); if (ms == NULL) { Py_DECREF(qs); @@ -12064,7 +12066,7 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, int steps=10; igraph_neimode_t mode = IGRAPH_OUT; igraph_random_walk_stuck_t stuck = IGRAPH_RANDOM_WALK_STUCK_RETURN; - igraph_vector_t walk; + igraph_vector_int_t walk; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiOO", kwlist, &start_o, &steps, &mode_o, &stuck_o)) @@ -12079,16 +12081,16 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_random_walk_stuck_t(stuck_o, &stuck)) return NULL; - if (igraph_vector_init(&walk, steps)) + if (igraph_vector_int_init(&walk, steps)) return igraphmodule_handle_igraph_error(); if (igraph_random_walk(&self->g, &walk, start, mode, steps, stuck)) { - igraph_vector_destroy(&walk); + igraph_vector_int_destroy(&walk); return igraphmodule_handle_igraph_error(); } - res = igraphmodule_vector_t_to_PyList(&walk, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&walk); + res = igraphmodule_vector_int_t_to_PyList(&walk); + igraph_vector_int_destroy(&walk); return res; } diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index d26a62229..31946354e 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -378,7 +378,7 @@ PyObject* igraphmodule_compare_communities(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "comm1", "comm2", "method", NULL }; PyObject *comm1_o, *comm2_o, *method_o = Py_None; - igraph_vector_t comm1, comm2; + igraph_vector_int_t comm1, comm2; igraph_community_comparison_t method = IGRAPH_COMMCMP_VI; igraph_real_t result; @@ -389,21 +389,21 @@ PyObject* igraphmodule_compare_communities(PyObject *self, if (igraphmodule_PyObject_to_community_comparison_t(method_o, &method)) return NULL; - if (igraphmodule_PyObject_to_vector_t(comm1_o, &comm1, 0)) + if (igraphmodule_PyObject_to_vector_int_t(comm1_o, &comm1)) return NULL; - if (igraphmodule_PyObject_to_vector_t(comm2_o, &comm2, 0)) { - igraph_vector_destroy(&comm1); + if (igraphmodule_PyObject_to_vector_int_t(comm2_o, &comm2)) { + igraph_vector_int_destroy(&comm1); return NULL; } if (igraph_compare_communities(&comm1, &comm2, &result, method)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&comm1); - igraph_vector_destroy(&comm2); + igraph_vector_int_destroy(&comm1); + igraph_vector_int_destroy(&comm2); return NULL; } - igraph_vector_destroy(&comm1); - igraph_vector_destroy(&comm2); + igraph_vector_int_destroy(&comm1); + igraph_vector_int_destroy(&comm2); return PyFloat_FromDouble((double)result); } @@ -570,28 +570,28 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, PyObject *args, PyObject *kwds) { static char* kwlist[] = { "comm1", "comm2", NULL }; PyObject *comm1_o, *comm2_o; - igraph_vector_t comm1, comm2; + igraph_vector_int_t comm1, comm2; igraph_integer_t distance12, distance21; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &comm1_o, &comm2_o)) return NULL; - if (igraphmodule_PyObject_to_vector_t(comm1_o, &comm1, 0)) + if (igraphmodule_PyObject_to_vector_int_t(comm1_o, &comm1)) return NULL; - if (igraphmodule_PyObject_to_vector_t(comm2_o, &comm2, 0)) { - igraph_vector_destroy(&comm1); + if (igraphmodule_PyObject_to_vector_int_t(comm2_o, &comm2)) { + igraph_vector_int_destroy(&comm1); return NULL; } if (igraph_split_join_distance(&comm1, &comm2, &distance12, &distance21)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&comm1); - igraph_vector_destroy(&comm2); + igraph_vector_int_destroy(&comm1); + igraph_vector_int_destroy(&comm2); return NULL; } - igraph_vector_destroy(&comm1); - igraph_vector_destroy(&comm2); + igraph_vector_int_destroy(&comm1); + igraph_vector_int_destroy(&comm2); return Py_BuildValue("ll", (long)distance12, (long)distance21); } diff --git a/tests/test_foreign.py b/tests/test_foreign.py index a3db0b869..5d5243e4b 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -4,7 +4,10 @@ from igraph import Graph, InternalError -from .utils import temporary_file +try: + from .utils import temporary_file +except ImportError: + from utils import temporary_file try: import networkx as nx @@ -277,20 +280,18 @@ def testAdjacency(self): ) as tmpfname: g = Graph.Read_Adjacency(tmpfname) self.assertTrue(isinstance(g, Graph)) - self.assertTrue( - g.vcount() == 6 - and g.ecount() == 18 - and g.is_directed() - and "weight" not in g.edge_attributes() - ) + self.assertEqual(g.vcount(), 6) + self.assertEqual(g.ecount(), 18) + self.assertTrue(g.is_directed()) + self.assertTrue("weight" not in g.edge_attributes()) + g = Graph.Read_Adjacency(tmpfname, attribute="weight") self.assertTrue(isinstance(g, Graph)) + self.assertEqual(g.vcount(), 6) + self.assertEqual(g.ecount(), 12) + self.assertTrue(g.is_directed()) self.assertTrue( - g.vcount() == 6 - and g.ecount() == 12 - and g.is_directed() - and g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2] - ) + g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]) g.write_adjacency(tmpfname) diff --git a/tests/test_games.py b/tests/test_games.py index 5b1a35ed1..5f7723790 100644 --- a/tests/test_games.py +++ b/tests/test_games.py @@ -24,7 +24,8 @@ def testRecentDegree(self): def testPreference(self): g = Graph.Preference(100, [1, 1], [[1, 0], [0, 1]]) - self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 2) + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(len(g.clusters()), 2) g = Graph.Preference(100, [1, 1], [[1, 0], [0, 1]], attribute="type") l = g.vs.get_attribute_values("type") @@ -32,7 +33,8 @@ def testPreference(self): def testAsymmetricPreference(self): g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[0, 1], [1, 0]]) - self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 2) + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(len(g.clusters()), 2) g = Graph.Asymmetric_Preference( 100, [[0, 1], [1, 0]], [[1, 0], [0, 1]], attribute="type" @@ -43,7 +45,8 @@ def testAsymmetricPreference(self): self.assertTrue(min(l1) == 0 and max(l1) == 1 and min(l2) == 0 and max(l2) == 1) g = Graph.Asymmetric_Preference(100, [[0, 1], [1, 0]], [[1, 0], [0, 1]]) - self.assertTrue(isinstance(g, Graph) and len(g.clusters()) == 1) + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(len(g.clusters()), 1) def testTreeGame(self): # Prufer algorithm From f78cbcbb2640a1cf842e59c7980bfdb6b49062fb Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 11 Aug 2021 21:51:41 +1000 Subject: [PATCH 0343/1681] Passes all tests except callback_function one --- src/_igraph/bfsiter.c | 8 +- src/_igraph/bfsiter.h | 2 +- src/_igraph/convert.c | 137 +++++++++++++++++- src/_igraph/convert.h | 3 + src/_igraph/dfsiter.c | 8 +- src/_igraph/dfsiter.h | 2 +- src/_igraph/graphobject.c | 293 +++++++++++++++++++------------------- src/_igraph/operators.c | 8 +- tests/test_isomorphism.py | 8 +- tests/test_structural.py | 6 +- tests/test_vertexseq.py | 5 +- 11 files changed, 309 insertions(+), 171 deletions(-) diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 9f7d12331..c86c90702 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -65,7 +65,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_vector_init(&o->neis, 0)) { + if (igraph_vector_int_init(&o->neis, 0)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); igraph_dqueue_destroy(&o->queue); return NULL; @@ -80,7 +80,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_dqueue_push(&o->queue, 0) || igraph_dqueue_push(&o->queue, -1)) { igraph_dqueue_destroy(&o->queue); - igraph_vector_destroy(&o->neis); + igraph_vector_int_destroy(&o->neis); PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } @@ -132,7 +132,7 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { Py_XDECREF(tmp); igraph_dqueue_destroy(&self->queue); - igraph_vector_destroy(&self->neis); + igraph_vector_int_destroy(&self->neis); free(self->visited); self->visited=0; @@ -168,7 +168,7 @@ PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { return NULL; } - for (i=0; ineis); i++) { + for (i=0; ineis); i++) { igraph_integer_t neighbor = (igraph_integer_t)VECTOR(self->neis)[i]; if (self->visited[neighbor]==0) { self->visited[neighbor]=1; diff --git a/src/_igraph/bfsiter.h b/src/_igraph/bfsiter.h index 94ddb6113..e36f03069 100644 --- a/src/_igraph/bfsiter.h +++ b/src/_igraph/bfsiter.h @@ -35,7 +35,7 @@ typedef struct PyObject_HEAD igraphmodule_GraphObject* gref; igraph_dqueue_t queue; - igraph_vector_t neis; + igraph_vector_int_t neis; igraph_t *graph; char *visited; igraph_neimode_t mode; diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 7f3e626e6..397794ede 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1348,6 +1348,40 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { return list; } +/** + * \ingroup python_interface_conversion + * \brief Converts an igraph \c igraph_vector_int_t to a Python integer list, with nan + * + * \param v the \c igraph_vector_int_t containing the vector to be converted + * \return the Python integer list as a \c PyObject*, or \c NULL if an error occurred + */ +PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t *v, const igraph_integer_t nanvalue) { + PyObject *list, *item; + Py_ssize_t n, i; + igraph_integer_t val; + + n = igraph_vector_int_size(v); + if (n<0) + return igraphmodule_handle_igraph_error(); + + list=PyList_New(n); + for (i=0; ineis, 0)) { + if (igraph_vector_int_init(&o->neis, 0)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); igraph_stack_destroy(&o->stack); return NULL; @@ -81,7 +81,7 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_stack_push(&o->stack, 0) || igraph_stack_push(&o->stack, -1)) { igraph_stack_destroy(&o->stack); - igraph_vector_destroy(&o->neis); + igraph_vector_int_destroy(&o->neis); PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } @@ -133,7 +133,7 @@ int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { Py_XDECREF(tmp); igraph_stack_destroy(&self->stack); - igraph_vector_destroy(&self->neis); + igraph_vector_int_destroy(&self->neis); free(self->visited); self->visited = 0; @@ -193,7 +193,7 @@ PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { igraphmodule_handle_igraph_error(); return NULL; } - for (i=0; ineis); i++) { + for (i=0; ineis); i++) { igraph_integer_t neighbor = (igraph_integer_t)VECTOR(self->neis)[i]; /* new neighbor, push the next item onto the stack */ if (self->visited[neighbor] == 0) { diff --git a/src/_igraph/dfsiter.h b/src/_igraph/dfsiter.h index 0a094c592..757caf798 100644 --- a/src/_igraph/dfsiter.h +++ b/src/_igraph/dfsiter.h @@ -35,7 +35,7 @@ typedef struct PyObject_HEAD igraphmodule_GraphObject* gref; igraph_stack_t stack; - igraph_vector_t neis; + igraph_vector_int_t neis; igraph_t *graph; char *visited; igraph_neimode_t mode; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d96b2e93f..3f6736acf 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -1625,7 +1625,7 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, PyObject *dir = Py_True, *vcount_if_unconnected = Py_True, *result; PyObject *weights_o = Py_None; igraph_vector_t *weights = 0; - igraph_vector_t res; + igraph_vector_int_t res; static char *kwlist[] = { "directed", "unconn", "weights", NULL }; @@ -1637,7 +1637,7 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_vector_init(&res, 0); + igraph_vector_int_init(&res, 0); if (weights) { if (igraph_diameter_dijkstra(&self->g, weights, 0, /* from, to, vertex_path, edge_path */ @@ -1645,7 +1645,7 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, PyObject_IsTrue(dir), PyObject_IsTrue(vcount_if_unconnected))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(weights); free(weights); - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return NULL; } igraph_vector_destroy(weights); free(weights); @@ -1659,8 +1659,8 @@ PyObject *igraphmodule_Graph_get_diameter(igraphmodule_GraphObject * self, } } - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); + result = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return result; } @@ -2808,7 +2808,7 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, */ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, PyObject *args, PyObject *kwds) { - igraph_vector_t shifts; + igraph_vector_int_t shifts; long int repeats, n; PyObject *o_shifts; igraphmodule_GraphObject *self; @@ -2820,16 +2820,16 @@ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, &n, &o_shifts, &repeats)) return NULL; - if (igraphmodule_PyObject_to_vector_t(o_shifts, &shifts, 0)) + if (igraphmodule_PyObject_to_vector_int_t(o_shifts, &shifts)) return NULL; if (igraph_lcf_vector(&g, (igraph_integer_t) n, &shifts, (igraph_integer_t) repeats)) { - igraph_vector_destroy(&shifts); + igraph_vector_int_destroy(&shifts); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_destroy(&shifts); + igraph_vector_int_destroy(&shifts); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2844,8 +2844,8 @@ PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, PyObject *args, PyObject *kwds) { - igraph_vector_t outdeg, indeg; - igraph_vector_t *indegp = 0; + igraph_vector_int_t outdeg, indeg; + igraph_vector_int_t *indegp = 0; igraph_edge_type_sw_t allowed_edge_types = IGRAPH_SIMPLE_SW; igraph_realize_degseq_t method = IGRAPH_REALIZE_DEGSEQ_SMALLEST; PyObject *outdeg_o, *indeg_o = Py_None; @@ -2867,13 +2867,13 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, return NULL; /* Outdegree vector */ - if (igraphmodule_PyObject_to_vector_t(outdeg_o, &outdeg, 0)) + if (igraphmodule_PyObject_to_vector_int_t(outdeg_o, &outdeg)) return NULL; /* Indegree vector, PyNone means undirected graph */ if (indeg_o != Py_None) { - if (igraphmodule_PyObject_to_vector_t(indeg_o, &indeg, 0)) { - igraph_vector_destroy(&outdeg); + if (igraphmodule_PyObject_to_vector_int_t(indeg_o, &indeg)) { + igraph_vector_int_destroy(&outdeg); return NULL; } indegp = &indeg; @@ -2881,16 +2881,16 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, /* C function takes care of multi-sw and directed corner case */ if (igraph_realize_degree_sequence(&g, &outdeg, indegp, allowed_edge_types, method)) { - igraph_vector_destroy(&outdeg); + igraph_vector_int_destroy(&outdeg); if (indegp != 0) - igraph_vector_destroy(&indeg); + igraph_vector_int_destroy(&indeg); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_destroy(&outdeg); + igraph_vector_int_destroy(&outdeg); if (indegp != 0) - igraph_vector_destroy(&indeg); + igraph_vector_int_destroy(&indeg); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -3662,22 +3662,22 @@ PyObject *igraphmodule_Graph_Weighted_Adjacency(PyTypeObject * type, * \sa igraph_articulation_points */ PyObject *igraphmodule_Graph_articulation_points(igraphmodule_GraphObject *self) { - igraph_vector_t res; + igraph_vector_int_t res; PyObject *o; - if (igraph_vector_init(&res, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraph_articulation_points(&self->g, &res)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return NULL; } - igraph_vector_sort(&res); - o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); + igraph_vector_int_sort(&res); + o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return o; } @@ -3989,7 +3989,7 @@ PyObject *igraphmodule_Graph_bibcoupling(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_ptr_t components; - igraph_vector_t points; + igraph_vector_int_t points; igraph_bool_t return_articulation_points; igraph_integer_t no; PyObject *result, *aps=Py_False; @@ -4004,7 +4004,7 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se return NULL; } if (return_articulation_points) { - if (igraph_vector_init(&points, 0)) { + if (igraph_vector_int_init(&points, 0)) { igraphmodule_handle_igraph_error(); igraph_vector_ptr_destroy(&components); return NULL; @@ -4014,19 +4014,19 @@ PyObject *igraphmodule_Graph_biconnected_components(igraphmodule_GraphObject *se if (igraph_biconnected_components(&self->g, &no, &components, 0, 0, return_articulation_points ? &points : 0)) { igraphmodule_handle_igraph_error(); igraph_vector_ptr_destroy(&components); - if (return_articulation_points) igraph_vector_destroy(&points); + if (return_articulation_points) igraph_vector_int_destroy(&points); return NULL; } - result = igraphmodule_vector_ptr_t_to_PyList(&components, IGRAPHMODULE_TYPE_INT); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&components, igraph_vector_destroy); + result = igraphmodule_vector_int_ptr_t_to_PyList(&components); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&components, igraph_vector_int_destroy); igraph_vector_ptr_destroy_all(&components); if (return_articulation_points) { PyObject *result2; - igraph_vector_sort(&points); - result2 = igraphmodule_vector_t_to_PyList(&points, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&points); + igraph_vector_int_sort(&points); + result2 = igraphmodule_vector_int_t_to_PyList(&points); + igraph_vector_int_destroy(&points); return Py_BuildValue("NN", result, result2); /* references stolen */ } @@ -4182,22 +4182,22 @@ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject * \sa igraph_bridges */ PyObject *igraphmodule_Graph_bridges(igraphmodule_GraphObject *self) { - igraph_vector_t res; + igraph_vector_int_t res; PyObject *o; - if (igraph_vector_init(&res, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraph_bridges(&self->g, &res)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return NULL; } igraph_vector_sort(&res); - o = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); + o = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return o; } @@ -5066,7 +5066,7 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * igraph_vs_destroy(&to); if (weights) { igraph_vector_destroy(weights); free(weights); } - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&res, igraph_vector_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&res, igraph_vector_int_destroy); j = igraph_vector_ptr_size(&res); list = PyList_New(j); @@ -5077,9 +5077,8 @@ PyObject *igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject * for (i = 0; i < j; i++) { item = - igraphmodule_vector_t_to_PyList((igraph_vector_t *) - igraph_vector_ptr_e(&res, i), - IGRAPHMODULE_TYPE_INT); + igraphmodule_vector_int_t_to_PyList( + (igraph_vector_int_t *)igraph_vector_ptr_e(&res, i)); if (!item) { Py_DECREF(list); igraph_vector_ptr_destroy_all(&res); @@ -5269,7 +5268,7 @@ PyObject *igraphmodule_Graph_neighborhood(igraphmodule_GraphObject *self, igraph_vs_destroy(&vs); if (!return_single) - result = igraphmodule_vector_ptr_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); + result = igraphmodule_vector_int_ptr_t_to_PyList(&res); else result = igraphmodule_vector_int_t_to_PyList((igraph_vector_int_t*)VECTOR(res)[0]); @@ -6192,7 +6191,7 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * PyObject *list, *mode_o=Py_None; PyObject *warnings_o=Py_True; igraph_neimode_t mode = IGRAPH_OUT; - igraph_vector_t result; + igraph_vector_int_t result; igraph_warning_handler_t* old_handler = 0; int retval; @@ -6200,7 +6199,7 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraph_vector_init(&result, 0)) + if (igraph_vector_int_init(&result, 0)) return igraphmodule_handle_igraph_error(); if (!PyObject_IsTrue(warnings_o)) { @@ -6217,12 +6216,12 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * if (retval) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } - list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + list = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return list; } @@ -6737,7 +6736,7 @@ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, igraph_matrix_t m; PyObject *result, *order_o = Py_None, *center_o = Py_None; igraph_integer_t center = 0; - igraph_vector_t* order = 0; + igraph_vector_int_t* order = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, ¢er_o, &order_o)) @@ -6752,13 +6751,13 @@ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, return NULL; if (order_o != Py_None) { - order = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); + order = (igraph_vector_int_t*)calloc(1, sizeof(igraph_vector_int_t)); if (!order) { igraph_matrix_destroy(&m); PyErr_NoMemory(); return NULL; } - if (igraphmodule_PyObject_to_vector_t(order_o, order, 1)) { + if (igraphmodule_PyObject_to_vector_int_t(order_o, order)) { igraph_matrix_destroy(&m); free(order); igraphmodule_handle_igraph_error(); @@ -6768,7 +6767,7 @@ PyObject *igraphmodule_Graph_layout_star(igraphmodule_GraphObject* self, if (igraph_layout_star(&self->g, &m, center, order)) { if (order) { - igraph_vector_destroy(order); free(order); + igraph_vector_int_destroy(order); free(order); } igraph_matrix_destroy(&m); igraphmodule_handle_igraph_error(); @@ -7407,8 +7406,8 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford(igraphmodule_GraphObject { static char *kwlist[] = { "mode", "root", "rootlevel", NULL }; igraph_matrix_t m; - igraph_vector_t roots, *roots_p = 0; - igraph_vector_t rootlevels, *rootlevels_p = 0; + igraph_vector_int_t roots, *roots_p = 0; + igraph_vector_int_t rootlevels, *rootlevels_p = 0; PyObject *roots_o=Py_None, *rootlevels_o=Py_None, *mode_o=Py_None; igraph_neimode_t mode = IGRAPH_OUT; PyObject *result; @@ -7422,32 +7421,32 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford(igraphmodule_GraphObject if (roots_o != Py_None) { roots_p = &roots; - if (igraphmodule_PyObject_to_vector_t(roots_o, roots_p, 1)) return 0; + if (igraphmodule_PyObject_to_vector_int_t(roots_o, roots_p)) return 0; } if (rootlevels_o != Py_None) { rootlevels_p = &rootlevels; - if (igraphmodule_PyObject_to_vector_t(rootlevels_o, rootlevels_p, 1)) { - if (roots_p) igraph_vector_destroy(roots_p); + if (igraphmodule_PyObject_to_vector_int_t(rootlevels_o, rootlevels_p)) { + if (roots_p) igraph_vector_int_destroy(roots_p); return 0; } } if (igraph_matrix_init(&m, 1, 1)) { - if (roots_p) igraph_vector_destroy(roots_p); - if (rootlevels_p) igraph_vector_destroy(rootlevels_p); + if (roots_p) igraph_vector_int_destroy(roots_p); + if (rootlevels_p) igraph_vector_int_destroy(rootlevels_p); igraphmodule_handle_igraph_error(); return NULL; } if (igraph_layout_reingold_tilford(&self->g, &m, mode, roots_p, rootlevels_p)) { igraph_matrix_destroy(&m); - if (roots_p) igraph_vector_destroy(roots_p); - if (rootlevels_p) igraph_vector_destroy(rootlevels_p); + if (roots_p) igraph_vector_int_destroy(roots_p); + if (rootlevels_p) igraph_vector_int_destroy(rootlevels_p); igraphmodule_handle_igraph_error(); return NULL; } - if (roots_p) igraph_vector_destroy(roots_p); - if (rootlevels_p) igraph_vector_destroy(rootlevels_p); + if (roots_p) igraph_vector_int_destroy(roots_p); + if (rootlevels_p) igraph_vector_int_destroy(rootlevels_p); result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); @@ -8723,7 +8722,7 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, PyObject *sho1=Py_None, *sho2=Py_None; PyObject *color1_o=Py_None, *color2_o=Py_None; igraphmodule_GraphObject *other; - igraph_vector_t mapping_12, mapping_21, *map12=0, *map21=0; + igraph_vector_int_t mapping_12, mapping_21, *map12=0, *map21=0; igraph_bliss_sh_t sh1=IGRAPH_BLISS_FL, sh2=IGRAPH_BLISS_FL; igraph_vector_int_t *color1=0, *color2=0; int retval; @@ -8752,11 +8751,11 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, if (o == Py_None) other = self; else other = (igraphmodule_GraphObject *) o; if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); + igraph_vector_int_init(&mapping_12, 0); map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); + igraph_vector_int_init(&mapping_21, 0); map21 = &mapping_21; } @@ -8779,17 +8778,17 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, iso = result ? Py_True : Py_False; Py_INCREF(iso); if (map12) { - m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map12); + m1 = igraphmodule_vector_int_t_to_PyList(map12); + igraph_vector_int_destroy(map12); if (!m1) { Py_DECREF(iso); - if (map21) igraph_vector_destroy(map21); + if (map21) igraph_vector_int_destroy(map21); return NULL; } } else { m1 = Py_None; Py_INCREF(m1); } if (map21) { - m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map21); + m2 = igraphmodule_vector_int_t_to_PyList(map21); + igraph_vector_int_destroy(map21); if (!m2) { Py_DECREF(iso); Py_DECREF(m1); return NULL; @@ -8809,7 +8808,7 @@ typedef struct { } igraphmodule_i_Graph_isomorphic_vf2_callback_data_t; igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( - const igraph_vector_t *map12, const igraph_vector_t *map21, + const igraph_vector_int_t *map12, const igraph_vector_int_t *map21, void* extra) { igraphmodule_i_Graph_isomorphic_vf2_callback_data_t* data = (igraphmodule_i_Graph_isomorphic_vf2_callback_data_t*)extra; @@ -8817,14 +8816,14 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( PyObject *map12_o, *map21_o; PyObject *result; - map12_o = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); + map12_o = igraphmodule_vector_int_t_to_PyList(map12); if (map12_o == NULL) { /* Error in conversion, return 0 to stop the search */ PyErr_WriteUnraisable(data->callback_fn); return 0; } - map21_o = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); + map21_o = igraphmodule_vector_int_t_to_PyList(map21); if (map21_o == NULL) { /* Error in conversion, return 0 to stop the search */ PyErr_WriteUnraisable(data->callback_fn); @@ -8916,8 +8915,7 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, PyObject *callback_fn=Py_None; PyObject *node_compat_fn=Py_None, *edge_compat_fn=Py_None; igraphmodule_GraphObject *other; - igraph_vector_t mapping_12, mapping_21; - igraph_vector_t *map12=0, *map21=0; + igraph_vector_int_t mapping_12, mapping_21, *map12=0, *map21=0; igraph_vector_int_t *color1=0, *color2=0; igraph_vector_int_t *edge_color1=0, *edge_color2=0; igraphmodule_i_Graph_isomorphic_vf2_callback_data_t callback_data; @@ -8977,11 +8975,11 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); + igraph_vector_int_init(&mapping_12, 0); map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); + igraph_vector_int_init(&mapping_21, 0); map21 = &mapping_21; } @@ -9022,16 +9020,16 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, } else { PyObject *m1, *m2; if (map12) { - m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map12); + m1 = igraphmodule_vector_int_t_to_PyList(map12); + igraph_vector_int_destroy(map12); if (!m1) { - if (map21) igraph_vector_destroy(map21); + if (map21) igraph_vector_int_destroy(map21); return NULL; } } else { m1 = Py_None; Py_INCREF(m1); } if (map21) { - m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map21); + m2 = igraphmodule_vector_int_t_to_PyList(map21); + igraph_vector_int_destroy(map21); if (!m2) { Py_DECREF(m1); return NULL; @@ -9232,9 +9230,9 @@ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } if (edge_color2) { igraph_vector_int_destroy(edge_color2); free(edge_color2); } - res = igraphmodule_vector_ptr_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); + res = igraphmodule_vector_int_ptr_t_to_PyList(&result); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&result, igraph_vector_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&result, igraph_vector_int_destroy); igraph_vector_ptr_destroy_all(&result); return res; @@ -9585,7 +9583,7 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, igraphmodule_GraphObject *other; igraph_vector_ptr_t domains; igraph_vector_ptr_t* p_domains = 0; - igraph_vector_t mapping, *map=0; + igraph_vector_int_t mapping, *map=0; static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", "return_mapping", NULL }; @@ -9598,14 +9596,14 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, other=(igraphmodule_GraphObject*)o; if (domains_o != Py_None) { - if (igraphmodule_PyObject_to_vector_ptr_t(domains_o, &domains, 1)) + if (igraphmodule_PyObject_to_vector_int_ptr_t(domains_o, &domains)) return NULL; p_domains = &domains; } if (PyObject_IsTrue(return_mapping)) { - if (igraph_vector_init(&mapping, 0)) { + if (igraph_vector_int_init(&mapping, 0)) { if (p_domains) igraph_vector_ptr_destroy_all(p_domains); igraphmodule_handle_igraph_error(); @@ -9630,8 +9628,8 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, Py_RETURN_TRUE; Py_RETURN_FALSE; } else { - PyObject *m = igraphmodule_vector_t_to_PyList(map, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map); + PyObject *m = igraphmodule_vector_int_t_to_PyList(map); + igraph_vector_int_destroy(map); if (!m) return NULL; return Py_BuildValue("ON", result ? Py_True : Py_False, m); @@ -9663,7 +9661,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( other=(igraphmodule_GraphObject*)o; if (domains_o != Py_None) { - if (igraphmodule_PyObject_to_vector_ptr_t(domains_o, &domains, 1)) + if (igraphmodule_PyObject_to_vector_int_ptr_t(domains_o, &domains)) return NULL; p_domains = &domains; @@ -9688,7 +9686,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( if (p_domains) igraph_vector_ptr_destroy_all(p_domains); - result = igraphmodule_vector_ptr_t_to_PyList(&mappings, IGRAPHMODULE_TYPE_INT); + result = igraphmodule_vector_int_ptr_t_to_PyList(&mappings); igraph_vector_ptr_destroy_all(&mappings); return result; @@ -9946,22 +9944,23 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, long vid; PyObject *l1, *l2, *l3, *result, *mode_o=Py_None; igraph_neimode_t mode = IGRAPH_OUT; - igraph_vector_t vids; - igraph_vector_t layers; - igraph_vector_t parents; + igraph_vector_int_t vids; + igraph_vector_int_t layers; + igraph_vector_int_t parents; if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|O", kwlist, &vid, &mode_o)) return NULL; if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraph_vector_init(&vids, igraph_vcount(&self->g))) + if (igraph_vector_int_init(&vids, igraph_vcount(&self->g))) return igraphmodule_handle_igraph_error(); - if (igraph_vector_init(&layers, igraph_vcount(&self->g))) { - igraph_vector_destroy(&vids); + if (igraph_vector_int_init(&layers, igraph_vcount(&self->g))) { + igraph_vector_int_destroy(&vids); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&parents, igraph_vcount(&self->g))) { - igraph_vector_destroy(&vids); igraph_vector_destroy(&parents); + if (igraph_vector_int_init(&parents, igraph_vcount(&self->g))) { + igraph_vector_int_destroy(&vids); + igraph_vector_int_destroy(&parents); return igraphmodule_handle_igraph_error(); } if (igraph_bfs_simple @@ -9969,9 +9968,9 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, igraphmodule_handle_igraph_error(); return NULL; } - l1 = igraphmodule_vector_t_to_PyList(&vids, IGRAPHMODULE_TYPE_INT); - l2 = igraphmodule_vector_t_to_PyList(&layers, IGRAPHMODULE_TYPE_INT); - l3 = igraphmodule_vector_t_to_PyList(&parents, IGRAPHMODULE_TYPE_INT); + l1 = igraphmodule_vector_int_t_to_PyList(&vids); + l2 = igraphmodule_vector_int_t_to_PyList(&layers); + l3 = igraphmodule_vector_int_t_to_PyList(&parents); if (l1 && l2 && l3) { result = Py_BuildValue("NNN", l1, l2, l3); /* references stolen */ } else { @@ -9980,9 +9979,9 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, if (l3) { Py_DECREF(l3); } result = NULL; } - igraph_vector_destroy(&vids); - igraph_vector_destroy(&layers); - igraph_vector_destroy(&parents); + igraph_vector_int_destroy(&vids); + igraph_vector_int_destroy(&layers); + igraph_vector_int_destroy(&parents); return result; } @@ -10096,7 +10095,7 @@ PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, PyObject *list = Py_None; PyObject *mode_o = Py_None; long int root = -1; - igraph_vector_t dom; + igraph_vector_int_t dom; igraph_neimode_t mode = IGRAPH_OUT; int res ; @@ -10111,16 +10110,18 @@ PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, mode = IGRAPH_OUT; } - if (igraph_vector_init(&dom, 0)) { + if (igraph_vector_int_init(&dom, 0)) { return NULL; } res = igraph_dominator_tree(&self->g, root, &dom, NULL, NULL, mode); if(res) { - igraph_vector_destroy(&dom); + igraph_vector_int_destroy(&dom); return NULL; } - list = igraphmodule_vector_t_to_PyList(&dom, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&dom); + /* The igraph API uses -2 for vertices that are not reachable from the root, + * but the Python API seems to be using nan judging from the unit tests */ + list = igraphmodule_vector_int_t_to_PyList_with_nan(&dom, -2); + igraph_vector_int_destroy(&dom); return list; } @@ -10286,17 +10287,17 @@ PyObject *igraphmodule_Graph_all_st_cuts(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&cuts, igraph_vector_destroy); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&partition1s, igraph_vector_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&cuts, igraph_vector_int_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&partition1s, igraph_vector_int_destroy); - cuts_o = igraphmodule_vector_ptr_t_to_PyList(&cuts, IGRAPHMODULE_TYPE_INT); + cuts_o = igraphmodule_vector_int_ptr_t_to_PyList(&cuts); igraph_vector_ptr_destroy_all(&cuts); if (cuts_o == NULL) { igraph_vector_ptr_destroy_all(&partition1s); return NULL; } - partition1s_o = igraphmodule_vector_ptr_t_to_PyList(&partition1s, IGRAPHMODULE_TYPE_INT); + partition1s_o = igraphmodule_vector_int_ptr_t_to_PyList(&partition1s); igraph_vector_ptr_destroy_all(&partition1s); if (partition1s_o == NULL) return NULL; @@ -10353,17 +10354,17 @@ PyObject *igraphmodule_Graph_all_st_mincuts(igraphmodule_GraphObject * self, igraph_vector_destroy(&capacity_vector); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&cuts, igraph_vector_destroy); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&partition1s, igraph_vector_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&cuts, igraph_vector_int_destroy); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&partition1s, igraph_vector_int_destroy); - cuts_o = igraphmodule_vector_ptr_t_to_PyList(&cuts, IGRAPHMODULE_TYPE_INT); + cuts_o = igraphmodule_vector_int_ptr_t_to_PyList(&cuts); igraph_vector_ptr_destroy_all(&cuts); if (cuts_o == NULL) { igraph_vector_ptr_destroy_all(&partition1s); return NULL; } - partition1s_o = igraphmodule_vector_ptr_t_to_PyList(&partition1s, IGRAPHMODULE_TYPE_INT); + partition1s_o = igraphmodule_vector_int_ptr_t_to_PyList(&partition1s); igraph_vector_ptr_destroy_all(&partition1s); if (partition1s_o == NULL) return NULL; @@ -10837,8 +10838,8 @@ PyObject *igraphmodule_Graph_cohesive_blocks(igraphmodule_GraphObject *self, return NULL; } - blocks_o = igraphmodule_vector_ptr_t_to_PyList(&blocks, IGRAPHMODULE_TYPE_INT); - IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&blocks, igraph_vector_destroy); + blocks_o = igraphmodule_vector_int_ptr_t_to_PyList(&blocks); + IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&blocks, igraph_vector_int_destroy); igraph_vector_ptr_destroy_all(&blocks); if (blocks_o == NULL) { igraph_vector_int_destroy(&parents); @@ -11280,7 +11281,7 @@ PyObject *igraphmodule_Graph_coreness(igraphmodule_GraphObject * self, { static char *kwlist[] = { "mode", NULL }; igraph_neimode_t mode = IGRAPH_ALL; - igraph_vector_t result; + igraph_vector_int_t result; PyObject *o, *mode_o = Py_None; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -11288,16 +11289,16 @@ PyObject *igraphmodule_Graph_coreness(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraph_vector_init(&result, igraph_vcount(&self->g))) + if (igraph_vector_int_init(&result, igraph_vcount(&self->g))) return igraphmodule_handle_igraph_error(); if (igraph_coreness(&self->g, &result, mode)) { - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return igraphmodule_handle_igraph_error(); } - o = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + o = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return o; } @@ -11312,7 +11313,7 @@ PyObject *igraphmodule_Graph_coreness(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = {"membership", "weights", "resolution", "directed", 0}; - igraph_vector_t membership; + igraph_vector_int_t membership; igraph_vector_t *weights=0; double resolution = 1; igraph_real_t modularity; @@ -11322,23 +11323,23 @@ PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OdO", kwlist, &mvec, &wvec, &resolution, &directed)) return NULL; - if (igraphmodule_PyObject_to_vector_t(mvec, &membership, 1)) + if (igraphmodule_PyObject_to_vector_int_t(mvec, &membership)) return NULL; if (igraphmodule_attrib_to_vector_t(wvec, self, &weights, ATTRIBUTE_TYPE_EDGE)){ - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); return NULL; } if (igraph_modularity(&self->g, &membership, weights, resolution, PyObject_IsTrue(directed), &modularity)) { - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); if (weights) { igraph_vector_destroy(weights); free(weights); } return NULL; } - igraph_vector_destroy(&membership); + igraph_vector_int_destroy(&membership); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -11354,7 +11355,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject PyObject *directed = Py_True; PyObject *weights_o = Py_None; PyObject *res, *qs, *ms; - igraph_matrix_t merges; + igraph_matrix_int_t merges; igraph_vector_t q; igraph_vector_t *weights = 0; @@ -11364,7 +11365,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) return NULL; - if (igraph_matrix_init(&merges, 0, 0)) { + if (igraph_matrix_int_init(&merges, 0, 0)) { if (weights != 0) { igraph_vector_destroy(weights); free(weights); } @@ -11372,7 +11373,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject } if (igraph_vector_init(&q, 0)) { - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); if (weights != 0) { igraph_vector_destroy(weights); free(weights); } @@ -11392,7 +11393,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); igraph_vector_destroy(&q); return NULL; } @@ -11407,7 +11408,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject qs=igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&q); if (!qs) { - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); return NULL; } } else { @@ -11415,8 +11416,8 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject Py_INCREF(qs); } - ms=igraphmodule_matrix_t_to_PyList(&merges, IGRAPHMODULE_TYPE_INT); - igraph_matrix_destroy(&merges); + ms=igraphmodule_matrix_int_t_to_PyList(&merges); + igraph_matrix_int_destroy(&merges); if (ms == NULL) { Py_DECREF(qs); @@ -11508,7 +11509,7 @@ PyObject *igraphmodule_Graph_community_fastgreedy(igraphmodule_GraphObject * sel { static char *kwlist[] = { "weights", NULL }; PyObject *ms, *qs, *res, *weights = Py_None; - igraph_matrix_t merges; + igraph_matrix_int_t merges; igraph_vector_t q, *ws=0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &weights)) { @@ -11518,14 +11519,14 @@ PyObject *igraphmodule_Graph_community_fastgreedy(igraphmodule_GraphObject * sel if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) return NULL; - igraph_matrix_init(&merges, 0, 0); + igraph_matrix_int_init(&merges, 0, 0); igraph_vector_init(&q, 0); if (igraph_community_fastgreedy(&self->g, ws, &merges, &q, 0)) { if (ws) { igraph_vector_destroy(ws); free(ws); } igraph_vector_destroy(&q); - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); return igraphmodule_handle_igraph_error(); } if (ws) { @@ -11539,7 +11540,7 @@ PyObject *igraphmodule_Graph_community_fastgreedy(igraphmodule_GraphObject * sel return NULL; } - ms=igraphmodule_matrix_t_to_PyList(&merges, IGRAPHMODULE_TYPE_INT); + ms=igraphmodule_matrix_int_t_to_PyList(&merges); igraph_matrix_destroy(&merges); if (ms == NULL) { @@ -11638,8 +11639,8 @@ PyObject *igraphmodule_Graph_community_label_propagation( static char *kwlist[] = { "weights", "initial", "fixed", NULL }; PyObject *weights_o = Py_None, *initial_o = Py_None, *fixed_o = Py_None; PyObject *result; - igraph_vector_int_t membership; - igraph_vector_t *ws = 0, *initial = 0; + igraph_vector_int_t membership, *initial = 0; + igraph_vector_t *ws = 0; igraph_vector_bool_t fixed; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &weights_o, &initial_o, &fixed_o)) { @@ -11656,7 +11657,7 @@ PyObject *igraphmodule_Graph_community_label_propagation( return NULL; } - if (igraphmodule_attrib_to_vector_t(initial_o, self, &initial, ATTRIBUTE_TYPE_VERTEX)){ + if (igraphmodule_attrib_to_vector_int_t(initial_o, self, &initial, ATTRIBUTE_TYPE_VERTEX)){ if (fixed_o != Py_None) igraph_vector_bool_destroy(&fixed); if (ws) { igraph_vector_destroy(ws); free(ws); } return NULL; @@ -11667,14 +11668,14 @@ PyObject *igraphmodule_Graph_community_label_propagation( ws, initial, (fixed_o != Py_None ? &fixed : 0), 0)) { if (fixed_o != Py_None) igraph_vector_bool_destroy(&fixed); if (ws) { igraph_vector_destroy(ws); free(ws); } - if (initial) { igraph_vector_destroy(initial); free(initial); } + if (initial) { igraph_vector_int_destroy(initial); free(initial); } igraph_vector_int_destroy(&membership); return igraphmodule_handle_igraph_error(); } if (fixed_o != Py_None) igraph_vector_bool_destroy(&fixed); if (ws) { igraph_vector_destroy(ws); free(ws); } - if (initial) { igraph_vector_destroy(initial); free(initial); } + if (initial) { igraph_vector_int_destroy(initial); free(initial); } result=igraphmodule_vector_int_t_to_PyList(&membership); igraph_vector_int_destroy(&membership); diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c index e748e7a39..7d8db55bb 100644 --- a/src/_igraph/operators.c +++ b/src/_igraph/operators.c @@ -153,10 +153,10 @@ PyObject *igraphmodule__union(PyObject *self, for (i = 0; i < no_of_graphs; i++) { long int j; long int no_of_edges = (long int) igraph_ecount(VECTOR(gs)[i]); - igraph_vector_t *map = VECTOR(edgemaps)[i]; + igraph_vector_int_t *map = VECTOR(edgemaps)[i]; PyObject *emi = PyList_New((Py_ssize_t) no_of_edges); for (j = 0; j < no_of_edges; j++) { - PyObject *dest = PyLong_FromLong(VECTOR(*map)[j]); + PyObject *dest = PyLong_FromLong((long)VECTOR(*map)[j]); PyList_SET_ITEM(emi, (Py_ssize_t) j, dest); } PyList_SET_ITEM(em_list, (Py_ssize_t) i, emi); @@ -263,10 +263,10 @@ PyObject *igraphmodule__intersection(PyObject *self, for (i = 0; i < no_of_graphs; i++) { long int j; long int no_of_edges = (long int) igraph_ecount(VECTOR(gs)[i]); - igraph_vector_t *map = VECTOR(edgemaps)[i]; + igraph_vector_int_t *map = VECTOR(edgemaps)[i]; PyObject *emi = PyList_New((Py_ssize_t) no_of_edges); for (j = 0; j < no_of_edges; j++) { - PyObject *dest = PyLong_FromLong(VECTOR(*map)[j]); + PyObject *dest = PyLong_FromLong((long)VECTOR(*map)[j]); PyList_SET_ITEM(emi, (Py_ssize_t) j, dest); } PyList_SET_ITEM(em_list, (Py_ssize_t) i, emi); diff --git a/tests/test_isomorphism.py b/tests/test_isomorphism.py index d5042ec83..d7416db9e 100644 --- a/tests/test_isomorphism.py +++ b/tests/test_isomorphism.py @@ -411,9 +411,11 @@ def suite(): isomorphism_suite = unittest.makeSuite(IsomorphismTests) subisomorphism_suite = unittest.makeSuite(SubisomorphismTests) permutation_suite = unittest.makeSuite(PermutationTests) - return unittest.TestSuite( - [isomorphism_suite, subisomorphism_suite, permutation_suite] - ) + return unittest.TestSuite([ + isomorphism_suite, + subisomorphism_suite, + permutation_suite, + ]) def test(): diff --git a/tests/test_structural.py b/tests/test_structural.py index 9ea13d990..edec2f8f4 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -498,11 +498,11 @@ def testNeighborhoodSize(self): class MiscTests(unittest.TestCase): def testBridges(self): g = Graph(5, [(0, 1), (1, 2), (2, 0), (0, 3), (3, 4)]) - self.assertTrue(g.bridges() == [3, 4]) + self.assertEqual(g.bridges(), [3, 4]) g = Graph(7, [(0, 1), (1, 2), (2, 0), (1, 6), (1, 3), (1, 4), (3, 5), (4, 5)]) - self.assertTrue(g.bridges() == [3]) + self.assertEqual(g.bridges(), [3]) g = Graph(3, [(0, 1), (1, 2), (2, 3)]) - self.assertTrue(g.bridges() == [0, 1, 2]) + self.assertEqual(g.bridges(), [0, 1, 2]) def testConstraint(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) diff --git a/tests/test_vertexseq.py b/tests/test_vertexseq.py index 12adf1aa1..89c9e8896 100644 --- a/tests/test_vertexseq.py +++ b/tests/test_vertexseq.py @@ -4,7 +4,10 @@ from igraph import * -from .utils import is_pypy +try: + from .utils import is_pypy +except ImportError: + from utils import is_pypy try: import numpy as np From 7c9f04aee529e744eeb539d57af924ecb493c435 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 12 Aug 2021 05:46:43 +1000 Subject: [PATCH 0344/1681] Compiler warnings, except one callback --- src/_igraph/convert.c | 29 ++++- src/_igraph/convert.h | 1 + src/_igraph/graphobject.c | 265 +++++++++++++++++++------------------- 3 files changed, 162 insertions(+), 133 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 397794ede..965a8f532 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1384,11 +1384,38 @@ PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t /** * \ingroup python_interface_conversion - * \brief Converts an igraph \c igraph_vector_int_t to a Python integer tuple + * \brief Converts an igraph \c igraph_vector_t to a Python integer tuple * * \param v the \c igraph_vector_t containing the vector to be converted * \return the Python integer tuple as a \c PyObject*, or \c NULL if an error occurred */ +PyObject* igraphmodule_vector_t_to_PyTuple(const igraph_vector_t *v) { + PyObject* tuple; + Py_ssize_t n, i; + + n=igraph_vector_size(v); + if (n<0) return igraphmodule_handle_igraph_error(); + + tuple=PyTuple_New(n); + for (i=0; ig, &girth, &vids)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&vids); + igraph_vector_int_destroy(&vids); return NULL; } if (PyObject_IsTrue(sc)) { PyObject* o; - o=igraphmodule_vector_t_to_PyList(&vids, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&vids); + o=igraphmodule_vector_int_t_to_PyList(&vids); + igraph_vector_int_destroy(&vids); return o; } return PyLong_FromLong((long)girth); @@ -2024,7 +2024,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, igraph_t g; long n, m = 1; float power = 1.0f, zero_appeal = 1.0f; - igraph_vector_t outseq; + igraph_vector_int_t outseq; igraph_t *start_from = 0; igraph_barabasi_algorithm_t algo = IGRAPH_BARABASI_PSUMTREE; PyObject *m_obj = 0, *outpref = Py_False, *directed = Py_False; @@ -2054,15 +2054,15 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, } if (m_obj == 0) { - igraph_vector_init(&outseq, 0); + igraph_vector_int_init(&outseq, 0); m = 1; } else if (m_obj != 0) { /* let's check whether we have a constant out-degree or a list */ if (PyLong_Check(m_obj)) { m = PyLong_AsLong(m_obj); - igraph_vector_init(&outseq, 0); + igraph_vector_int_init(&outseq, 0); } else if (PyList_Check(m_obj)) { - if (igraphmodule_PyObject_to_vector_t(m_obj, &outseq, 1)) { + if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { /* something bad happened during conversion */ return NULL; } @@ -2080,11 +2080,11 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, PyObject_IsTrue(directed), algo, start_from)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&outseq); + igraph_vector_int_destroy(&outseq); return NULL; } - igraph_vector_destroy(&outseq); + igraph_vector_int_destroy(&outseq); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -2102,7 +2102,7 @@ PyObject *igraphmodule_Graph_Bipartite(PyTypeObject * type, igraphmodule_GraphObject *self; igraph_t g; igraph_vector_bool_t types; - igraph_vector_t edges; + igraph_vector_int_t edges; igraph_bool_t edges_owned = 0; PyObject *types_o, *edges_o, *directed = Py_False; @@ -2123,14 +2123,14 @@ PyObject *igraphmodule_Graph_Bipartite(PyTypeObject * type, if (igraph_create_bipartite(&g, &types, &edges, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } igraph_vector_bool_destroy(&types); return NULL; } if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } igraph_vector_bool_destroy(&types); @@ -2177,7 +2177,7 @@ PyObject *igraphmodule_Graph_Degree_Sequence(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - igraph_vector_t outseq, inseq; + igraph_vector_int_t outseq, inseq; igraph_degseq_t meth = IGRAPH_DEGSEQ_SIMPLE; igraph_bool_t has_inseq = 0; PyObject *outdeg = NULL, *indeg = NULL, *method = NULL; @@ -2191,10 +2191,10 @@ PyObject *igraphmodule_Graph_Degree_Sequence(PyTypeObject * type, return NULL; if (igraphmodule_PyObject_to_degseq_t(method, &meth)) return NULL; - if (igraphmodule_PyObject_to_vector_t(outdeg, &outseq, 1)) return NULL; + if (igraphmodule_PyObject_to_vector_int_t(outdeg, &outseq)) return NULL; if (indeg) { - if (igraphmodule_PyObject_to_vector_t(indeg, &inseq, 1)) { - igraph_vector_destroy(&outseq); + if (igraphmodule_PyObject_to_vector_int_t(indeg, &inseq)) { + igraph_vector_int_destroy(&outseq); return NULL; } has_inseq=1; @@ -2202,15 +2202,15 @@ PyObject *igraphmodule_Graph_Degree_Sequence(PyTypeObject * type, if (igraph_degree_sequence_game(&g, &outseq, has_inseq ? &inseq : 0, meth)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&outseq); + igraph_vector_int_destroy(&outseq); if (has_inseq) - igraph_vector_destroy(&inseq); + igraph_vector_int_destroy(&inseq); return NULL; } - igraph_vector_destroy(&outseq); + igraph_vector_int_destroy(&outseq); if (has_inseq) - igraph_vector_destroy(&inseq); + igraph_vector_int_destroy(&inseq); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -3173,7 +3173,7 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, igraph_t g; long n, m = 0, window = 0; float power = 0.0f, zero_appeal = 0.0f; - igraph_vector_t outseq; + igraph_vector_int_t outseq; PyObject *m_obj, *outpref = Py_False, *directed = Py_False; char *kwlist[] = @@ -3193,10 +3193,10 @@ NULL }; // let's check whether we have a constant out-degree or a list if (PyLong_Check(m_obj)) { m = PyLong_AsLong(m_obj); - igraph_vector_init(&outseq, 0); + igraph_vector_int_init(&outseq, 0); } else if (PyList_Check(m_obj)) { - if (igraphmodule_PyObject_to_vector_t(m_obj, &outseq, 1)) { + if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { // something bad happened during conversion return NULL; } @@ -3210,11 +3210,11 @@ NULL }; (igraph_real_t) zero_appeal, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&outseq); + igraph_vector_int_destroy(&outseq); return NULL; } - igraph_vector_destroy(&outseq); + igraph_vector_int_destroy(&outseq); CREATE_GRAPH_FROM_TYPE(self, g, type); @@ -3691,18 +3691,18 @@ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *sel PyObject *types_o = Py_None, *directed = Py_True; igraph_real_t res; int ret; - igraph_vector_t *types = 0; + igraph_vector_int_t *types = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &types_o, &directed)) return NULL; - if (igraphmodule_attrib_to_vector_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) + if (igraphmodule_attrib_to_vector_int_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) return NULL; ret = igraph_assortativity_nominal(&self->g, types, &res, PyObject_IsTrue(directed)); if (types) { - igraph_vector_destroy(types); free(types); + igraph_vector_int_destroy(types); free(types); } if (ret) { @@ -4195,7 +4195,7 @@ PyObject *igraphmodule_Graph_bridges(igraphmodule_GraphObject *self) { return NULL; } - igraph_vector_sort(&res); + igraph_vector_int_sort(&res); o = igraphmodule_vector_int_t_to_PyList(&res); igraph_vector_int_destroy(&res); @@ -4850,7 +4850,7 @@ PyObject *igraphmodule_Graph_feedback_arc_set( igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "weights", "method", NULL }; igraph_vector_t* weights = 0; - igraph_vector_t result; + igraph_vector_int_t result; igraph_fas_algorithm_t algo = IGRAPH_FAS_APPROX_EADES; PyObject *weights_o = Py_None, *result_o = NULL, *algo_o = NULL; @@ -4864,20 +4864,20 @@ PyObject *igraphmodule_Graph_feedback_arc_set( ATTRIBUTE_TYPE_EDGE)) return NULL; - if (igraph_vector_init(&result, 0)) { + if (igraph_vector_int_init(&result, 0)) { if (weights) { igraph_vector_destroy(weights); free(weights); } } if (igraph_feedback_arc_set(&self->g, &result, weights, algo)) { if (weights) { igraph_vector_destroy(weights); free(weights); } - igraph_vector_destroy(&result); + igraph_vector_int_destroy(&result); return NULL; } if (weights) { igraph_vector_destroy(weights); free(weights); } - result_o = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&result); + result_o = igraphmodule_vector_int_t_to_PyList(&result); + igraph_vector_int_destroy(&result); return result_o; } @@ -4893,7 +4893,8 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * PyObject * kwds) { static char *kwlist[] = { "v", "to", "weights", "mode", "output", NULL }; - igraph_vector_t *res, *weights=0; + igraph_vector_t *weights=0; + igraph_vector_int_t *res; igraph_neimode_t mode = IGRAPH_OUT; long int i, j; igraph_integer_t from, no_of_target_nodes; @@ -4953,7 +4954,6 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * return NULL; } - // FIXME: is this alright? res = (igraph_vector_int_t *) calloc(no_of_target_nodes, sizeof(igraph_vector_int_t)); if (!res) { PyErr_SetString(PyExc_MemoryError, ""); @@ -5123,7 +5123,7 @@ PyObject *igraphmodule_Graph_get_all_simple_paths(igraphmodule_GraphObject * if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (PyLong_AsInt(cutoff_o, &cutoff)) + if (igraphmodule_PyObject_to_integer_t(cutoff_o, &cutoff)) return NULL; if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) @@ -5708,15 +5708,15 @@ PyObject *igraphmodule_Graph_similarity_jaccard(igraphmodule_GraphObject * self, igraph_matrix_destroy(&res); } else { /* Case #2: vertex pairs or edges, returning list */ - igraph_vector_t edges; + igraph_vector_int_t edges; igraph_vector_t res; igraph_bool_t edges_owned; if (igraphmodule_PyObject_to_edgelist(pairs_o, &edges, 0, &edges_owned)) return NULL; - if (igraph_vector_init(&res, igraph_vector_size(&edges) / 2)) { - igraph_vector_destroy(&edges); + if (igraph_vector_init(&res, igraph_vector_int_size(&edges) / 2)) { + igraph_vector_int_destroy(&edges); igraphmodule_handle_igraph_error(); return NULL; } @@ -5725,14 +5725,14 @@ PyObject *igraphmodule_Graph_similarity_jaccard(igraphmodule_GraphObject * self, PyObject_IsTrue(loops))) { igraph_vector_destroy(&res); if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } igraphmodule_handle_igraph_error(); return NULL; } if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); @@ -5794,16 +5794,16 @@ PyObject *igraphmodule_Graph_similarity_dice(igraphmodule_GraphObject * self, igraph_matrix_destroy(&res); } else { /* Case #2: vertex pairs or edges, returning list */ - igraph_vector_t edges; + igraph_vector_int_t edges; igraph_vector_t res; igraph_bool_t edges_owned; if (igraphmodule_PyObject_to_edgelist(pairs_o, &edges, 0, &edges_owned)) return NULL; - if (igraph_vector_init(&res, igraph_vector_size(&edges) / 2)) { + if (igraph_vector_init(&res, igraph_vector_int_size(&edges) / 2)) { if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } igraphmodule_handle_igraph_error(); return NULL; @@ -5813,14 +5813,14 @@ PyObject *igraphmodule_Graph_similarity_dice(igraphmodule_GraphObject * self, PyObject_IsTrue(loops))) { igraph_vector_destroy(&res); if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } igraphmodule_handle_igraph_error(); return NULL; } if (edges_owned) { - igraph_vector_destroy(&edges); + igraph_vector_int_destroy(&edges); } list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_FLOAT); @@ -5883,32 +5883,32 @@ PyObject *igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject * self, { static char *kwlist[] = { "weights", NULL }; igraph_vector_t* ws = 0; - igraph_vector_t res; + igraph_vector_int_t res; PyObject *weights_o = Py_None, *result = NULL; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &weights_o)) return NULL; - if (igraph_vector_init(&res, 0)) { + if (igraph_vector_int_init(&res, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &ws, ATTRIBUTE_TYPE_EDGE)) { - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return NULL; } if (igraph_minimum_spanning_tree(&self->g, &res, ws)) { if (ws != 0) { igraph_vector_destroy(ws); free(ws); } - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); igraphmodule_handle_igraph_error(); return NULL; } if (ws != 0) { igraph_vector_destroy(ws); free(ws); } - result = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); + result = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return result; } @@ -5953,7 +5953,7 @@ PyObject *igraphmodule_Graph_subcomponent(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "v", "mode", NULL }; - igraph_vector_t res; + igraph_vector_int_t res; igraph_neimode_t mode = IGRAPH_ALL; igraph_integer_t from; PyObject *list = NULL, *mode_o = Py_None, *from_o = Py_None; @@ -5967,15 +5967,15 @@ PyObject *igraphmodule_Graph_subcomponent(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_vid(from_o, &from, &self->g)) return NULL; - igraph_vector_init(&res, 0); + igraph_vector_int_init(&res, 0); if (igraph_subcomponent(&self->g, &res, from, mode)) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&res); + igraph_vector_int_destroy(&res); return NULL; } - list = igraphmodule_vector_t_to_PyList(&res, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&res); + list = igraphmodule_vector_int_t_to_PyList(&res); + igraph_vector_int_destroy(&res); return list; } @@ -6527,17 +6527,19 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s } } else { /* samples given in advance */ - igraph_vector_t samp; - if (igraphmodule_PyObject_to_vector_t(sample, &samp, 1)) { + igraph_vector_int_t samp; + if (igraphmodule_PyObject_to_vector_int_t(sample, &samp)) { igraph_vector_destroy(&cut_prob); return NULL; } if (igraph_motifs_randesu_estimate(&self->g, &result, (igraph_integer_t) size, &cut_prob, 0, &samp)) { igraphmodule_handle_igraph_error(); + igraph_vector_int_destroy(&samp); igraph_vector_destroy(&cut_prob); return NULL; } + igraph_vector_int_destroy(&samp); } igraph_vector_destroy(&cut_prob); @@ -6550,20 +6552,20 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s * \sa igraph_triad_census */ PyObject *igraphmodule_Graph_triad_census(igraphmodule_GraphObject *self) { - igraph_vector_int_t result; + igraph_vector_t result; PyObject *list; - if (igraph_vector_int_init(&result, 16)) { + if (igraph_vector_init(&result, 16)) { return igraphmodule_handle_igraph_error(); } if (igraph_triad_census(&self->g, &result)) { igraphmodule_handle_igraph_error(); - igraph_vector_int_destroy(&result); + igraph_vector_destroy(&result); return NULL; } - list = igraphmodule_vector_int_t_to_PyTuple(&result); - igraph_vector_int_destroy(&result); + list = igraphmodule_vector_t_to_PyTuple(&result); + igraph_vector_destroy(&result); return list; } @@ -7464,8 +7466,8 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford_circular( { static char *kwlist[] = { "mode", "root", "rootlevel", NULL }; igraph_matrix_t m; - igraph_vector_t roots, *roots_p = 0; - igraph_vector_t rootlevels, *rootlevels_p = 0; + igraph_vector_int_t roots, *roots_p = 0; + igraph_vector_int_t rootlevels, *rootlevels_p = 0; PyObject *roots_o=Py_None, *rootlevels_o=Py_None, *mode_o=Py_None; igraph_neimode_t mode = IGRAPH_OUT; PyObject *result; @@ -7478,19 +7480,19 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford_circular( if (roots_o != Py_None) { roots_p = &roots; - if (igraphmodule_PyObject_to_vector_t(roots_o, roots_p, 1)) return 0; + if (igraphmodule_PyObject_to_vector_int_t(roots_o, roots_p)) return 0; } if (rootlevels_o != Py_None) { rootlevels_p = &rootlevels; - if (igraphmodule_PyObject_to_vector_t(rootlevels_o, rootlevels_p, 1)) { - if (roots_p) igraph_vector_destroy(roots_p); + if (igraphmodule_PyObject_to_vector_int_t(rootlevels_o, rootlevels_p)) { + if (roots_p) igraph_vector_int_destroy(roots_p); return 0; } } if (igraph_matrix_init(&m, 1, 1)) { - if (roots_p) igraph_vector_destroy(roots_p); - if (rootlevels_p) igraph_vector_destroy(rootlevels_p); + if (roots_p) igraph_vector_int_destroy(roots_p); + if (rootlevels_p) igraph_vector_int_destroy(rootlevels_p); igraphmodule_handle_igraph_error(); return NULL; } @@ -7498,13 +7500,13 @@ PyObject *igraphmodule_Graph_layout_reingold_tilford_circular( if (igraph_layout_reingold_tilford_circular(&self->g, &m, mode, roots_p, rootlevels_p)) { igraph_matrix_destroy(&m); - if (roots_p) igraph_vector_destroy(roots_p); - if (rootlevels_p) igraph_vector_destroy(rootlevels_p); + if (roots_p) igraph_vector_int_destroy(roots_p); + if (rootlevels_p) igraph_vector_int_destroy(rootlevels_p); igraphmodule_handle_igraph_error(); return NULL; } - if (roots_p) igraph_vector_destroy(roots_p); - if (rootlevels_p) igraph_vector_destroy(rootlevels_p); + if (roots_p) igraph_vector_int_destroy(roots_p); + if (rootlevels_p) igraph_vector_int_destroy(rootlevels_p); result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); @@ -7523,8 +7525,9 @@ PyObject *igraphmodule_Graph_layout_sugiyama( "return_extended_graph", NULL }; igraph_matrix_t m; igraph_t extd_graph; - igraph_vector_t extd_to_orig_eids; - igraph_vector_t *weights = 0, *layers = 0; + igraph_vector_int_t extd_to_orig_eids; + igraph_vector_t *weights = 0; + igraph_vector_int_t *layers = 0; double hgap = 1, vgap = 1; long int maxiter = 100; PyObject *layers_o = Py_None, *weights_o = Py_None, *extd_to_orig_eids_o = Py_None; @@ -7536,28 +7539,28 @@ PyObject *igraphmodule_Graph_layout_sugiyama( &layers_o, &weights_o, &hgap, &vgap, &maxiter, &return_extended_graph)) return NULL; - if (igraph_vector_init(&extd_to_orig_eids, 0)) { + if (igraph_vector_int_init(&extd_to_orig_eids, 0)) { igraphmodule_handle_igraph_error(); return NULL; } if (igraph_matrix_init(&m, 1, 1)) { - igraph_vector_destroy(&extd_to_orig_eids); + igraph_vector_int_destroy(&extd_to_orig_eids); igraphmodule_handle_igraph_error(); return NULL; } - if (igraphmodule_attrib_to_vector_t(layers_o, self, &layers, + if (igraphmodule_attrib_to_vector_int_t(layers_o, self, &layers, ATTRIBUTE_TYPE_VERTEX)) { - igraph_vector_destroy(&extd_to_orig_eids); + igraph_vector_int_destroy(&extd_to_orig_eids); igraph_matrix_destroy(&m); return NULL; } if (igraphmodule_attrib_to_vector_t(weights_o, self, &weights, ATTRIBUTE_TYPE_EDGE)) { - if (layers != 0) { igraph_vector_destroy(layers); free(layers); } - igraph_vector_destroy(&extd_to_orig_eids); + if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } + igraph_vector_int_destroy(&extd_to_orig_eids); igraph_matrix_destroy(&m); return NULL; } @@ -7566,15 +7569,15 @@ PyObject *igraphmodule_Graph_layout_sugiyama( (PyObject_IsTrue(return_extended_graph) ? &extd_graph : 0), (PyObject_IsTrue(return_extended_graph) ? &extd_to_orig_eids : 0), layers, hgap, vgap, maxiter, weights)) { - if (layers != 0) { igraph_vector_destroy(layers); free(layers); } + if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } - igraph_vector_destroy(&extd_to_orig_eids); + igraph_vector_int_destroy(&extd_to_orig_eids); igraph_matrix_destroy(&m); igraphmodule_handle_igraph_error(); return NULL; } - if (layers != 0) { igraph_vector_destroy(layers); free(layers); } + if (layers != 0) { igraph_vector_int_destroy(layers); free(layers); } if (weights != 0) { igraph_vector_destroy(weights); free(weights); } result = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); @@ -7582,12 +7585,11 @@ PyObject *igraphmodule_Graph_layout_sugiyama( if (PyObject_IsTrue(return_extended_graph)) { CREATE_GRAPH(graph_o, extd_graph); - extd_to_orig_eids_o = igraphmodule_vector_t_to_PyList(&extd_to_orig_eids, - IGRAPHMODULE_TYPE_INT); + extd_to_orig_eids_o = igraphmodule_vector_int_t_to_PyList(&extd_to_orig_eids); result = Py_BuildValue("NNN", result, graph_o, extd_to_orig_eids_o); } - igraph_vector_destroy(&extd_to_orig_eids); + igraph_vector_int_destroy(&extd_to_orig_eids); return (PyObject *) result; } @@ -8598,8 +8600,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( PyObject *color_o = Py_None; PyObject *list; igraph_bliss_sh_t sh = IGRAPH_BLISS_FL; - igraph_vector_t labeling; - igraph_vector_int_t *color = 0; + igraph_vector_int_t labeling, *color = 0; int retval; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &sh_o, &color_o)) @@ -8608,7 +8609,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( if (igraphmodule_PyObject_to_bliss_sh_t(sh_o, &sh)) return NULL; - if (igraph_vector_init(&labeling, 0)) { + if (igraph_vector_int_init(&labeling, 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -8622,13 +8623,13 @@ PyObject *igraphmodule_Graph_canonical_permutation( if (retval) { igraphmodule_handle_igraph_error(); - igraph_vector_destroy(&labeling); + igraph_vector_int_destroy(&labeling); return NULL; } - list = igraphmodule_vector_t_to_PyList(&labeling, IGRAPHMODULE_TYPE_INT); + list = igraphmodule_vector_int_t_to_PyList(&labeling); - igraph_vector_destroy(&labeling); + igraph_vector_int_destroy(&labeling); return list; } @@ -8657,10 +8658,10 @@ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, } if (vids) { - igraph_vector_t vidsvec; - if (igraphmodule_PyObject_to_vector_t(vids, &vidsvec, 1)) { + igraph_vector_int_t vidsvec; + if (igraphmodule_PyObject_to_vector_int_t(vids, &vidsvec)) { PyErr_SetString(PyExc_ValueError, - "Error while converting PyList to igraph_vector_t"); + "Error while converting PyList to igraph_vector_int_t"); return NULL; } if (igraph_isoclass_subgraph(&self->g, &vidsvec, &isoclass)) { @@ -9254,7 +9255,7 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, PyObject *callback_fn=Py_None; PyObject *node_compat_fn=Py_None, *edge_compat_fn=Py_None; igraphmodule_GraphObject *other; - igraph_vector_t mapping_12, mapping_21, *map12=0, *map21=0; + igraph_vector_int_t mapping_12, mapping_21, *map12=0, *map21=0; igraph_vector_int_t *color1=0, *color2=0; igraph_vector_int_t *edge_color1=0, *edge_color2=0; igraphmodule_i_Graph_isomorphic_vf2_callback_data_t callback_data; @@ -9310,11 +9311,11 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, } if (PyObject_IsTrue(return1)) { - igraph_vector_init(&mapping_12, 0); + igraph_vector_int_init(&mapping_12, 0); map12 = &mapping_12; } if (PyObject_IsTrue(return2)) { - igraph_vector_init(&mapping_21, 0); + igraph_vector_int_init(&mapping_21, 0); map21 = &mapping_21; } @@ -9356,18 +9357,18 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, } else { PyObject *m1, *m2; if (map12) { - m1 = igraphmodule_vector_t_to_PyList(map12, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map12); + m1 = igraphmodule_vector_int_t_to_PyList(map12); + igraph_vector_int_destroy(map12); if (!m1) { - if (map21) igraph_vector_destroy(map21); + if (map21) igraph_vector_int_destroy(map21); return NULL; } } else { m1 = Py_None; Py_INCREF(m1); } if (map21) { - m2 = igraphmodule_vector_t_to_PyList(map21, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(map21); + m2 = igraphmodule_vector_int_t_to_PyList(map21); + igraph_vector_int_destroy(map21); if (!m2) { Py_DECREF(m1); return NULL; @@ -10013,7 +10014,7 @@ PyObject *igraphmodule_Graph_unfold_tree(igraphmodule_GraphObject * self, PyObject *mapping_o, *mode_o=Py_None, *roots_o=Py_None; igraph_neimode_t mode = IGRAPH_OUT; igraph_vs_t vs; - igraph_vector_t mapping, vids; + igraph_vector_int_t mapping, vids; igraph_t result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &roots_o, &mode_o)) @@ -10022,37 +10023,37 @@ PyObject *igraphmodule_Graph_unfold_tree(igraphmodule_GraphObject * self, if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; if (igraphmodule_PyObject_to_vs_t(roots_o, &vs, &self->g, 0, 0)) return NULL; - if (igraph_vector_init(&mapping, igraph_vcount(&self->g))) { + if (igraph_vector_int_init(&mapping, igraph_vcount(&self->g))) { igraph_vs_destroy(&vs); return igraphmodule_handle_igraph_error(); } - if (igraph_vector_init(&vids, 0)) { + if (igraph_vector_int_init(&vids, 0)) { igraph_vs_destroy(&vs); - igraph_vector_destroy(&mapping); + igraph_vector_int_destroy(&mapping); return igraphmodule_handle_igraph_error(); } if (igraph_vs_as_vector(&self->g, vs, &vids)) { igraph_vs_destroy(&vs); - igraph_vector_destroy(&vids); - igraph_vector_destroy(&mapping); + igraph_vector_int_destroy(&vids); + igraph_vector_int_destroy(&mapping); return igraphmodule_handle_igraph_error(); } igraph_vs_destroy(&vs); if (igraph_unfold_tree(&self->g, &result, mode, &vids, &mapping)) { - igraph_vector_destroy(&vids); - igraph_vector_destroy(&mapping); + igraph_vector_int_destroy(&vids); + igraph_vector_int_destroy(&mapping); igraphmodule_handle_igraph_error(); return NULL; } - igraph_vector_destroy(&vids); + igraph_vector_int_destroy(&vids); - mapping_o = igraphmodule_vector_t_to_PyList(&mapping, IGRAPHMODULE_TYPE_INT); - igraph_vector_destroy(&mapping); + mapping_o = igraphmodule_vector_int_t_to_PyList(&mapping); + igraph_vector_int_destroy(&mapping); if (!mapping_o) { igraph_destroy(&result); @@ -10915,7 +10916,7 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { for (j = i; j < n; j++) - igraph_vector_destroy((igraph_vector_t *) VECTOR(result)[j]); + igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; @@ -10923,7 +10924,7 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, else { PyList_SET_ITEM(list, i, item); } - igraph_vector_destroy(vec); + igraph_vector_int_destroy(vec); } igraph_vector_ptr_destroy_all(&result); @@ -11438,7 +11439,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj PyObject *cl, *res, *merges, *weights_obj = Py_None; igraph_vector_int_t membership; igraph_vector_t *weights = 0; - igraph_matrix_t m; + igraph_matrix_int_t m; igraph_real_t q; igraphmodule_ARPACKOptionsObject *arpack_options; PyObject *arpack_options_o = igraphmodule_arpack_options_default; @@ -11451,7 +11452,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj if (igraph_vector_int_init(&membership, 0)) return igraphmodule_handle_igraph_error(); - if (igraph_matrix_init(&m, 0, 0)) { + if (igraph_matrix_int_init(&m, 0, 0)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&membership); return 0; @@ -11464,7 +11465,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj if (igraphmodule_attrib_to_vector_t(weights_obj, self, &weights, ATTRIBUTE_TYPE_EDGE)) { - igraph_matrix_destroy(&m); + igraph_matrix_int_destroy(&m); igraph_vector_int_destroy(&membership); return NULL; } @@ -11472,7 +11473,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; if (igraph_community_leading_eigenvector(&self->g, weights, &m, &membership, (igraph_integer_t) n, igraphmodule_ARPACKOptions_get(arpack_options), &q, 0, 0, 0, 0, 0, 0)){ - igraph_matrix_destroy(&m); + igraph_matrix_int_destroy(&m); igraph_vector_int_destroy(&membership); if (weights != 0) { igraph_vector_destroy(weights); free(weights); @@ -11487,12 +11488,12 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj cl = igraphmodule_vector_int_t_to_PyList(&membership); igraph_vector_int_destroy(&membership); if (cl == 0) { - igraph_matrix_destroy(&m); + igraph_matrix_int_destroy(&m); return 0; } - merges = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_INT); - igraph_matrix_destroy(&m); + merges = igraphmodule_matrix_int_t_to_PyList(&m); + igraph_matrix_int_destroy(&m); if (merges == 0) return 0; @@ -11536,12 +11537,12 @@ PyObject *igraphmodule_Graph_community_fastgreedy(igraphmodule_GraphObject * sel qs=igraphmodule_vector_t_to_PyList(&q, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&q); if (!qs) { - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); return NULL; } ms=igraphmodule_matrix_int_t_to_PyList(&merges); - igraph_matrix_destroy(&merges); + igraph_matrix_int_destroy(&merges); if (ms == NULL) { Py_DECREF(qs); From 3fa278754fbaae9bd18e7bc2c84719b775bd32e9 Mon Sep 17 00:00:00 2001 From: odidev Date: Tue, 31 Aug 2021 14:45:23 +0530 Subject: [PATCH 0345/1681] Add linux aarch64 wheels (#432) Signed-off-by: odidev --- .github/workflows/build.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 73259dc38..6a0affaf7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,6 +34,31 @@ jobs: with: path: ./wheelhouse/*.whl + build_aarch64_wheels: + name: Build wheels on Linux AArch64 + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + - name: Set up QEMU + id: qemu + uses: docker/setup-qemu-action@v1 + + - name: Build wheels + uses: joerick/cibuildwheel@v1.10.0 + env: + CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" + CIBW_ARCHS_LINUX: aarch64 + CIBW_BUILD: "*-manylinux_aarch64" + + - uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl + build_wheel_macos: name: Build wheels on macOS runs-on: macos-10.15 From 3195c71da5dd6fce24f5c6896f928fe7fa64b738 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 1 Sep 2021 23:38:09 +0200 Subject: [PATCH 0346/1681] chore: clean up MANIFEST.in now that the C core is also cleaner --- MANIFEST.in | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index bbfa86415..a5592a432 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,9 +5,5 @@ include scripts/mkdoc.sh include tests/*.py graft vendor/source/igraph -prune vendor/source/igraph/etc/abstracts -prune vendor/source/igraph/etc/papers -prune vendor/source/igraph/etc/presentations prune vendor/source/igraph/interfaces -prune vendor/source/igraph/vendor/simpleraytracer From c77b369c430792a7ae136bb699c3677072685a75 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 2 Sep 2021 07:37:54 +0200 Subject: [PATCH 0347/1681] chore: repo root directory cleanup --- .flake8 | 3 - .gitignore | 2 - COPYING | 340 ----------------------------------------------- MANIFEST.in | 1 - requirements.txt | 1 - tox.ini | 4 + 6 files changed, 4 insertions(+), 347 deletions(-) delete mode 100644 .flake8 delete mode 100644 COPYING delete mode 100644 requirements.txt diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e4e5eb32d..000000000 --- a/.flake8 +++ /dev/null @@ -1,3 +0,0 @@ -[flake8] -max-line-length = 88 -extend-ignore = E203, W503 diff --git a/.gitignore b/.gitignore index 918256eeb..323291f0c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,4 @@ src/igraph/*.so .vscode/ vendor/build/ vendor/install/ -Pipfile -Pipfile.lock diff --git a/COPYING b/COPYING deleted file mode 100644 index 3912109b5..000000000 --- a/COPYING +++ /dev/null @@ -1,340 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc. - 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Library General Public License instead.) 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 -this service 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 make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. 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. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -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 -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the 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 a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE 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. - - 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 -convey the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - 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 2 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, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -Also add information on how to contact you by electronic and paper mail. - -If the program is interactive, make it output a short notice like this -when it starts in an interactive mode: - - Gnomovision version 69, Copyright (C) year name of author - Gnomovision 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, the commands you use may -be called something other than `show w' and `show c'; they could even be -mouse-clicks or menu items--whatever suits your program. - -You should also get your employer (if you work as a programmer) or your -school, if any, to sign a "copyright disclaimer" for the program, if -necessary. Here is a sample; alter the names: - - Yoyodyne, Inc., hereby disclaims all copyright interest in the program - `Gnomovision' (which makes passes at compilers) written by James Hacker. - - , 1 April 1989 - Ty Coon, President of Vice - -This 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 Library General -Public License instead of this License. diff --git a/MANIFEST.in b/MANIFEST.in index a5592a432..65b187f3e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include src/_igraph/*.h include MANIFEST.in -include COPYING include scripts/mkdoc.sh include tests/*.py diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index d6e1198b1..000000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ --e . diff --git a/tox.ini b/tox.ini index bd0355f4d..805660bf5 100644 --- a/tox.ini +++ b/tox.ini @@ -25,3 +25,7 @@ passenv = PATH setenv = TESTING_IN_TOX=1 +[flake8] +max-line-length = 88 +extend-ignore = E203, W503 + From 7601bae07f3c29b281dfc3bf5b1d3d4c8783395f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 2 Sep 2021 09:48:15 +0200 Subject: [PATCH 0348/1681] tox: ensure that the plotting deps are installed, and switch to cairocffi --- setup.py | 2 +- tox.ini | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0b64a20f2..127a0863b 100644 --- a/setup.py +++ b/setup.py @@ -788,7 +788,7 @@ def use_educated_guess(self): packages=["igraph", "igraph.app", "igraph.drawing", "igraph.remote"], scripts=["scripts/igraph"], install_requires=["texttable>=1.6.2"], - extras_require={"plotting": ["pycairo>=1.18.0"]}, + extras_require={"plotting": ["cairocffi>=1.2.0"]}, headers=headers, platforms="ALL", keywords=[ diff --git a/tox.ini b/tox.ini index 805660bf5..d144c3963 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py36, py37, py38, py39, pypy, pypy3 +envlist = py36, py37, py38, py39, pypy3 [gh-actions] python = @@ -21,6 +21,7 @@ deps = numpy; platform_python_implementation != "PyPy" networkx; platform_python_implementation != "PyPy" pandas; platform_python_implementation != "PyPy" +extras = plotting passenv = PATH setenv = TESTING_IN_TOX=1 From f97e0ac7b0d200ed23e338835d2cd046c0232f72 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 08:44:28 +0200 Subject: [PATCH 0349/1681] fix: igraph_get_adjacency() does not have an 'eids' argument any more --- src/_igraph/graphobject.c | 8 ++++---- tests/test_conversion.py | 14 -------------- vendor/source/igraph | 2 +- 3 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index f99d1bef6..3f9b5b812 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -7659,12 +7659,12 @@ PyObject *igraphmodule_Graph_layout_bipartite( PyObject *igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - static char *kwlist[] = { "type", "eids", NULL }; + static char *kwlist[] = { "type", NULL }; igraph_get_adjacency_t mode = IGRAPH_GET_ADJACENCY_BOTH; igraph_matrix_t m; - PyObject *result, *mode_o = Py_None, *eids = Py_False; + PyObject *result, *mode_o = Py_None; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &eids)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) return NULL; if (igraphmodule_PyObject_to_get_adjacency_t(mode_o, &mode)) return NULL; @@ -7675,7 +7675,7 @@ PyObject *igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject * self, return NULL; } - if (igraph_get_adjacency(&self->g, &m, mode, PyObject_IsTrue(eids))) { + if (igraph_get_adjacency(&self->g, &m, mode)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&m); return NULL; diff --git a/tests/test_conversion.py b/tests/test_conversion.py index b8600e9a3..5bfd443bb 100644 --- a/tests/test_conversion.py +++ b/tests/test_conversion.py @@ -125,20 +125,6 @@ def testGetAdjacency(self): ] ) ) - self.assertTrue( - g.get_adjacency(eids=True) - == Matrix( - [ - [0, 1, 2, 3, 0, 0], - [1, 0, 0, 0, 4, 5], - [2, 0, 0, 0, 0, 0], - [3, 0, 0, 0, 0, 0], - [0, 4, 0, 0, 0, 0], - [0, 5, 0, 0, 0, 0], - ] - ) - - 1 - ) # Directed case g = Graph.Tree(6, 3, "tree_out") diff --git a/vendor/source/igraph b/vendor/source/igraph index 2ceb15db7..fb17862e3 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 2ceb15db7983a15e499844daddd1e1aa72cb0138 +Subproject commit fb17862e3c8096171449b1680581a3b1e55e8a29 From 6ea9ed089a27470a8ccab0b05b0ccadadc4e2d8a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 09:23:41 +0200 Subject: [PATCH 0350/1681] fix: all test cases pass on my machine, yay --- src/_igraph/attributes.c | 2 +- src/_igraph/convert.c | 1 + src/_igraph/edgeseqobject.c | 12 ++++++++++-- src/_igraph/graphobject.c | 26 +++++++++++++------------- src/_igraph/vertexseqobject.c | 6 ++++++ 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index fa7bdd3ed..7ffa56e73 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -413,7 +413,7 @@ static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, } /* Adding vertices */ -static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, long int nv, igraph_vector_ptr_t *attr) { +static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, igraph_integer_t nv, igraph_vector_ptr_t *attr) { /* Extend the end of every value in the vertex hash with nv pieces of None */ PyObject *key, *value, *dict; long int i, j, k, l; diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index d2032e986..c36664d2e 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1337,6 +1337,7 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { list=PyList_New(n); for (i=0; ig); if (igraphmodule_PyObject_to_vector_int_t(esobj, &v)) @@ -228,9 +228,17 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, } break; + case IGRAPH_ES_NONE: + break; + /* TODO: IGRAPH_ES_PAIRS, IGRAPH_ES_ADJ, IGRAPH_ES_PATH, IGRAPH_ES_MULTIPATH - someday :) They are unused yet in the Python interface */ + + default: + return PyErr_Format( + igraphmodule_InternalError, "unsupported edge selector type: %d", igraph_es_type(&self->es) + ); } if (idx < 0) { PyErr_SetString(PyExc_IndexError, "edge index out of range"); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 3f9b5b812..f5d46e09e 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -6352,18 +6352,18 @@ typedef struct { PyObject* graph; } igraphmodule_i_Graph_motifs_randesu_callback_data_t; -igraph_bool_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *graph, - igraph_vector_t *vids, int isoclass, void* extra) { +igraph_error_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *graph, + igraph_vector_int_t *vids, int isoclass, void* extra) { igraphmodule_i_Graph_motifs_randesu_callback_data_t* data = (igraphmodule_i_Graph_motifs_randesu_callback_data_t*)extra; PyObject* vector; PyObject* result; igraph_bool_t retval; - vector = igraphmodule_vector_t_to_PyList(vids, IGRAPHMODULE_TYPE_INT); + vector = igraphmodule_vector_int_t_to_PyList(vids); if (vector == NULL) { /* Error in conversion, return 1 */ - return 1; + return IGRAPH_FAILURE; } result = PyObject_CallFunction(data->func, "OOi", data->graph, vector, isoclass); @@ -6371,13 +6371,13 @@ igraph_bool_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *graph if (result == NULL) { /* Error in callback, return 1 */ - return 1; + return IGRAPH_FAILURE; } retval = PyObject_IsTrue(result); Py_DECREF(result); - return retval; + return retval ? IGRAPH_STOP : IGRAPH_SUCCESS; } /** \ingroup python_interface_graph @@ -8819,17 +8819,17 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( map12_o = igraphmodule_vector_int_t_to_PyList(map12); if (map12_o == NULL) { - /* Error in conversion, return 0 to stop the search */ + /* Error in conversion, return an error code */ PyErr_WriteUnraisable(data->callback_fn); - return 0; + return IGRAPH_FAILURE; } map21_o = igraphmodule_vector_int_t_to_PyList(map21); if (map21_o == NULL) { - /* Error in conversion, return 0 to stop the search */ + /* Error in conversion, return an error code */ PyErr_WriteUnraisable(data->callback_fn); Py_DECREF(map21_o); - return 0; + return IGRAPH_FAILURE; } result = PyObject_CallFunction(data->callback_fn, "OOOO", data->graph1, data->graph2, @@ -8838,15 +8838,15 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_callback_fn( Py_DECREF(map21_o); if (result == NULL) { - /* Error in callback, return 0 */ + /* Error in callback, return an error code */ PyErr_WriteUnraisable(data->callback_fn); - return 0; + return IGRAPH_FAILURE; } retval = PyObject_IsTrue(result); Py_DECREF(result); - return retval; + return retval ? IGRAPH_SUCCESS : IGRAPH_STOP; } igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 4b2e83a3b..08cc5a002 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -213,6 +213,8 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, idx = self->vs.data.vid; } break; + case IGRAPH_VS_NONE: + break; case IGRAPH_VS_SEQ: if (i < 0) { i = self->vs.data.seq.to - self->vs.data.seq.from + i; @@ -223,6 +225,10 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, break; /* TODO: IGRAPH_VS_ADJ, IGRAPH_VS_NONADJ - someday :) They are unused yet in the Python interface */ + default: + return PyErr_Format( + igraphmodule_InternalError, "unsupported vertex selector type: %d", igraph_vs_type(&self->vs) + ); } if (idx < 0) { From 251a89cd21ff44822c710ec135de79271659d6e6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 09:29:43 +0200 Subject: [PATCH 0351/1681] feat: added 'test' extra to install all dependencies needed for unit tests --- .github/workflows/build.yml | 1 + setup.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a0affaf7..83ef87eb1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,6 +3,7 @@ name: Build and test, upload to PyPI on release on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m unittest -vvv" + CIBW_TEST_EXTRAS: "test" CIBW_SKIP: "cp27-* pp27-* cp35-*" jobs: diff --git a/setup.py b/setup.py index 127a0863b..2aaa15a6c 100644 --- a/setup.py +++ b/setup.py @@ -788,7 +788,10 @@ def use_educated_guess(self): packages=["igraph", "igraph.app", "igraph.drawing", "igraph.remote"], scripts=["scripts/igraph"], install_requires=["texttable>=1.6.2"], - extras_require={"plotting": ["cairocffi>=1.2.0"]}, + extras_require={ + "plotting": ["cairocffi>=1.2.0"], + "test": ["networkx>=2.6.2", "numpy>=1.21.2", "pandas>=1.3.2", "scipy>=1.7.1"] + }, headers=headers, platforms="ALL", keywords=[ From 8c15abb6659ae470e030cafa6ca442cd2744c2f5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 09:36:18 +0200 Subject: [PATCH 0352/1681] style: blackened setup.py --- setup.py | 80 ++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index 2aaa15a6c..ced505305 100644 --- a/setup.py +++ b/setup.py @@ -265,26 +265,36 @@ def compile_in(self, source_folder, build_folder, install_folder): print("Running build...") # We are _not_ using a parallel build; this is intentional, see igraph/igraph#1755 - retcode = subprocess.call( - [cmake, "--build", ".", "--config", "Release"] - ) + retcode = subprocess.call([cmake, "--build", ".", "--config", "Release"]) if retcode: return False print("Installing build...") - retcode = subprocess.call([cmake, "--install", ".", "--prefix", str(install_folder), "--config", "Release"]) + retcode = subprocess.call( + [ + cmake, + "--install", + ".", + "--prefix", + str(install_folder), + "--config", + "Release", + ] + ) if retcode: return False pkgconfig_candidates = [ install_folder / "lib" / "pkgconfig" / "igraph.pc", - install_folder / "lib64" / "pkgconfig" / "igraph.pc" + install_folder / "lib64" / "pkgconfig" / "igraph.pc", ] for candidate in pkgconfig_candidates: if candidate.exists(): return self.parse_pkgconfig_file(candidate) - raise RuntimeError("no igraph.pc was found in the installation folder of igraph") + raise RuntimeError( + "no igraph.pc was found in the installation folder of igraph" + ) ########################################################################### @@ -395,12 +405,16 @@ def run(self): # Add extra libraries that may have been specified if "IGRAPH_EXTRA_LIBRARIES" in os.environ: - extra_libraries = os.environ["IGRAPH_EXTRA_LIBRARIES"].split(',') + extra_libraries = os.environ["IGRAPH_EXTRA_LIBRARIES"].split(",") buildcfg.libraries.extend(extra_libraries) # Override static specification based on environment variable if "IGRAPH_STATIC_EXTENSION" in os.environ: - if os.environ["IGRAPH_STATIC_EXTENSION"].lower() in ['true', '1', 'on']: + if os.environ["IGRAPH_STATIC_EXTENSION"].lower() in [ + "true", + "1", + "on", + ]: buildcfg.static_extension = True else: buildcfg.static_extension = False @@ -417,7 +431,9 @@ def run(self): # Add extra libraries that may have been specified if "IGRAPH_EXTRA_DYNAMIC_LIBRARIES" in os.environ: - extra_libraries = os.environ["IGRAPH_EXTRA_DYNAMIC_LIBRARIES"].split(',') + extra_libraries = os.environ[ + "IGRAPH_EXTRA_DYNAMIC_LIBRARIES" + ].split(",") buildcfg.libraries.extend(extra_libraries) # Remove C++ standard library as we will use the C++ linker @@ -485,24 +501,38 @@ def run(self): cwd = os.getcwd() try: os.chdir(igraph_source_repo) - version = subprocess.check_output("git describe", shell=True).decode("utf-8").strip() + version = ( + subprocess.check_output("git describe", shell=True) + .decode("utf-8") + .strip() + ) finally: os.chdir(cwd) # If we still don't have a version number, try to parse it from # include/igraph_version.h if not version: - version_header = os.path.join(igraph_build_dir, "include", "igraph_version.h") + version_header = os.path.join( + igraph_build_dir, "include", "igraph_version.h" + ) if not os.path.exists(version_header): - raise RuntimeError("You need to build the C core of igraph first before generating a source tarball of python-igraph") + raise RuntimeError( + "You need to build the C core of igraph first before generating a source tarball of python-igraph" + ) with open(version_header, "r") as fp: - lines = [line.strip() for line in fp if line.startswith("#define IGRAPH_VERSION ")] + lines = [ + line.strip() + for line in fp + if line.startswith("#define IGRAPH_VERSION ") + ] if len(lines) == 1: version = lines[0].split('"')[1] if not isinstance(version, str) or len(version) < 5: - raise RuntimeError(f"Cannot determine the version number of the C core in {igraph_source_repo}") + raise RuntimeError( + f"Cannot determine the version number of the C core in {igraph_source_repo}" + ) if not is_git_repo(igraph_source_repo): # python-igraph was extracted from an official tarball so @@ -515,9 +545,14 @@ def run(self): # Copy the generated parser sources from the build folder parser_dir = os.path.join(igraph_build_dir, "src", "io", "parsers") if os.path.isdir(parser_dir): - shutil.copytree(parser_dir, os.path.join(igraph_source_repo, "src", "io", "parsers")) + shutil.copytree( + parser_dir, + os.path.join(igraph_source_repo, "src", "io", "parsers"), + ) else: - raise RuntimeError(f"You need to build the C core of igraph first before generating a source tarball of python-igraph") + raise RuntimeError( + f"You need to build the C core of igraph first before generating a source tarball of python-igraph" + ) # Add a version file to the tarball with open(version_file, "w") as fp: @@ -700,7 +735,9 @@ def use_vendored_igraph(self): buildcfg.library_dirs.append(str(candidate)) break else: - raise RuntimeError("cannot detect igraph library dir within " + str(vendor_dir)) + raise RuntimeError( + "cannot detect igraph library dir within " + str(vendor_dir) + ) if not buildcfg.static_extension: buildcfg.static_extension = "only_igraph" @@ -754,7 +791,6 @@ def use_educated_guess(self): buildcfg.process_args_from_command_line() - # Define the extension sources = glob.glob(os.path.join("src", "_igraph", "*.c")) sources.append(os.path.join("src", "_igraph", "force_cpp_linker.cpp")) @@ -790,7 +826,13 @@ def use_educated_guess(self): install_requires=["texttable>=1.6.2"], extras_require={ "plotting": ["cairocffi>=1.2.0"], - "test": ["networkx>=2.6.2", "numpy>=1.21.2", "pandas>=1.3.2", "scipy>=1.7.1"] + "test": [ + "networkx>=2.6.2", + "numpy>=1.21.2", + "pandas>=1.3.2", + "scipy>=1.7.1", + "pytest>=6.2.5", + ], }, headers=headers, platforms="ALL", From 74d3771130bfedcff5a213e049096a3c57bc36b2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 09:36:30 +0200 Subject: [PATCH 0353/1681] test: use pytest as test runner --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 83ef87eb1..7d0354422 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,7 @@ name: Build and test, upload to PyPI on release on: [push, pull_request] env: - CIBW_TEST_COMMAND: "cd {project} && python -m unittest -vvv" + CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" CIBW_SKIP: "cp27-* pp27-* cp35-*" From fe946fd7808520b7b02aba49f553c148fb3a81cc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 10:03:13 +0200 Subject: [PATCH 0354/1681] fix: started sorting out integer conversions and getting rid of unnecessary casts --- src/_igraph/convert.c | 81 ++++++++++++++++++----------------- src/_igraph/vertexseqobject.c | 52 ++++++++++++---------- 2 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index c36664d2e..ad261fccf 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -794,13 +794,16 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { * \param v the result is stored here * \return 0 if everything was OK, 1 otherwise */ -int PyLong_to_integer_t(PyObject* obj, int* v) { +int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { if (IGRAPH_INTEGER_SIZE == 64) { - *v = (igraph_integer_t)PyLong_AsLong(obj); + /* here the assumption is that sizeof(long long) == 64 bits; anyhow, this + * is the widest integer type that we can convert a PyLong to so we cannot + * do any better than this */ + *v = PyLong_AsLongLong(obj); } else { - int dummy; - PyLong_AsInt(obj, &dummy); - *v = (igraph_integer_t)dummy; + int dummy; + PyLong_AsInt(obj, &dummy); + *v = (igraph_integer_t)dummy; } return 0; } @@ -815,7 +818,8 @@ int PyLong_to_integer_t(PyObject* obj, int* v) { * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { - int retval, num; + int retval; + igraph_integer_t num; if (object == NULL) { } else if (PyLong_Check(object)) { @@ -1093,10 +1097,10 @@ int igraphmodule_PyObject_float_to_vector_t(PyObject *list, igraph_vector_t *v) * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v) { - PyObject *item; - int value=0; + PyObject *it = 0, *item; + igraph_integer_t value = 0; Py_ssize_t i, j, k; - int ok, retval; + int ok; if (PyBaseString_Check(list)) { /* It is highly unlikely that a string (although it is a sequence) will @@ -1107,24 +1111,20 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v if (!PySequence_Check(list)) { /* try to use an iterator */ - PyObject *it = PyObject_GetIter(list); + it = PyObject_GetIter(list); if (it) { - PyObject *item; - igraph_vector_int_init(v, 0); + if (igraph_vector_int_init(v, 0)) { + igraphmodule_handle_igraph_error(); + Py_DECREF(it); + return 1; + } + while ((item = PyIter_Next(it)) != 0) { - ok = 1; if (!PyNumber_Check(item)) { PyErr_SetString(PyExc_TypeError, "iterable must return numbers"); - ok=0; + ok = 0; } else { - PyObject *item2 = PyNumber_Long(item); - if (item2 == 0) { - PyErr_SetString(PyExc_TypeError, "can't convert a list item to integer"); - ok = 0; - } else { - ok = (PyLong_AsInt(item, &value) == 0); - Py_DECREF(item2); - } + ok = (igraphmodule_PyObject_to_integer_t(item, &value) == 0); } if (ok == 0) { @@ -1133,6 +1133,7 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v Py_DECREF(it); return 1; } + if (igraph_vector_int_push_back(v, value)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(v); @@ -1140,43 +1141,44 @@ int igraphmodule_PyObject_to_vector_int_t(PyObject *list, igraph_vector_int_t *v Py_DECREF(it); return 1; } + Py_DECREF(item); } + Py_DECREF(it); - return 0; } else { PyErr_SetString(PyExc_TypeError, "sequence or iterable expected"); return 1; } + return 0; } - j=PySequence_Size(list); - igraph_vector_int_init(v, j); - for (i=0, k=0; i= igraph_vcount(&((igraphmodule_GraphObject*)g)->g)) { PyErr_SetString(PyExc_ValueError, "vertex index out of range"); return -1; } + igraph_vs_1(&vs, (igraph_integer_t)idx); } else { igraph_vector_int_t v; @@ -252,7 +258,7 @@ PyObject* igraphmodule_VertexSeq_attribute_names(igraphmodule_VertexSeqObject* s PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObject* self, PyObject* o) { igraphmodule_GraphObject *gr = self->gref; PyObject *result=0, *values, *item; - long int i, n; + igraph_integer_t i, n; if (!igraphmodule_attribute_name_check(o)) return 0; @@ -275,7 +281,7 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje result = PyList_New(n); if (!result) return 0; - for (i=0; ivs.data.vecptr)[i]); + for (i = 0; i < n; i++) { + item = PyList_GET_ITEM(values, VECTOR(*self->vs.data.vecptr)[i]); Py_INCREF(item); PyList_SET_ITEM(result, i, item); } + break; case IGRAPH_VS_SEQ: @@ -300,11 +307,12 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje result = PyList_New(n); if (!result) return 0; - for (i=0; ivs.data.seq.from+i); + for (i = 0; i < n; i++) { + item = PyList_GET_ITEM(values, self->vs.data.seq.from + i); Py_INCREF(item); PyList_SET_ITEM(result, i, item); } + break; default: @@ -315,7 +323,7 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje } PyObject* igraphmodule_VertexSeq_get_attribute_values_mapping(igraphmodule_VertexSeqObject *self, PyObject *o) { - long int index; + Py_ssize_t index; /* Handle integer indices according to the sequence protocol */ if (PyIndex_Check(o)) { @@ -350,7 +358,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb PyObject *dict, *list, *item; igraphmodule_GraphObject *gr; igraph_vector_int_t vs; - long i, j, n, no_of_nodes; + igraph_integer_t i, j, n, no_of_nodes; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_VERTEX]; @@ -382,10 +390,10 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb } n=PySequence_Size(values); - if (n<0) return -1; + if (n < 0) return -1; if (igraph_vs_type(&self->vs) == IGRAPH_VS_ALL) { - no_of_nodes = (long)igraph_vcount(&gr->g); + no_of_nodes = igraph_vcount(&gr->g); if (n == 0 && no_of_nodes > 0) { PyErr_SetString(PyExc_ValueError, "sequence must not be empty"); return -1; @@ -395,7 +403,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb list = PyDict_GetItem(dict, attrname); if (list != 0) { /* Yes, we have. Modify its items to the items found in values */ - for (i=0, j=0; i 0) { PyErr_SetString(PyExc_ValueError, "sequence must not be empty"); igraph_vector_int_destroy(&vs); @@ -454,7 +462,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb return -1; } /* No need to Py_INCREF(item), PySequence_GetItem returns a new reference */ - if (PyList_SetItem(list, (long)VECTOR(vs)[i], item)) { + if (PyList_SetItem(list, VECTOR(vs)[i], item)) { Py_DECREF(item); igraph_vector_int_destroy(&vs); return -1; @@ -465,7 +473,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb /* We don't have attributes with the given name yet. Create an entry * in the dict, create a new list, fill with None for vertices not in the * sequence and copy the rest */ - long n2 = igraph_vcount(&gr->g); + igraph_integer_t n2 = igraph_vcount(&gr->g); list = PyList_New(n2); if (list == 0) { igraph_vector_int_destroy(&vs); @@ -483,7 +491,7 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb Py_DECREF(list); return -1; } /* No need to Py_INCREF(item), PySequence_GetItem returns a new reference */ - PyList_SET_ITEM(list, (long)VECTOR(vs)[i], item); + PyList_SET_ITEM(list, VECTOR(vs)[i], item); /* PyList_SET_ITEM stole a reference to the item automatically */ } igraph_vector_int_destroy(&vs); @@ -591,8 +599,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject *args) { igraphmodule_VertexSeqObject *result; igraphmodule_GraphObject *gr; - igraph_integer_t igraph_idx; - long i, j, n, m; + igraph_integer_t igraph_idx, i, j, n, m; gr=self->gref; result=igraphmodule_VertexSeq_copy(self); @@ -681,15 +688,14 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, m = igraph_vector_int_size(&v2); for (; i= m || idx < 0) { PyErr_SetString(PyExc_ValueError, "vertex index out of range"); igraph_vector_int_destroy(&v); @@ -794,7 +800,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, igraph_vector_int_destroy(&v2); return NULL; } - if (igraph_vector_int_push_back(&v, VECTOR(v2)[(long int) igraph_idx])) { + if (igraph_vector_int_push_back(&v, VECTOR(v2)[igraph_idx])) { Py_DECREF(result); Py_DECREF(iter); igraphmodule_handle_igraph_error(); From 499642686e65feb7aec063bb5f5cb5bac01a3b60 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 10:11:12 +0200 Subject: [PATCH 0355/1681] fix: invalidate cached C core if it is updated --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d0354422..7ea5de7b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-${{ runner.os }}-${{ hashFiles('.gitmodules') }} + key: C-core-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python From 2e5bb504cf8c718149b5e356b2cce4adbef82127 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 10:15:06 +0200 Subject: [PATCH 0356/1681] fix: another attempt at cache invalidaiton --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7ea5de7b5..250fecafc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} + key: C-core-cache-v2-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python From 144876c5a39000819e64bb3ab5114074476e90d8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 14:49:18 +0200 Subject: [PATCH 0357/1681] fix: yet another attempt at cache invalidation --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 250fecafc..e8c785feb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -75,7 +75,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-cache-v2-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} + key: C-core-cache-v1-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python @@ -126,7 +126,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-build-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.gitmodules') }}- + key: C-core-build-v1-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - name: Cache VCPKG uses: actions/cache@v2 @@ -176,7 +176,7 @@ jobs: path: | vendor/build vendor/install - key: C-core-${{ runner.os }}-${{ hashFiles('.gitmodules') }}-4 + key: C-core-v1-${{ runner.os }}-${{ hashFiles('.gitmodules') }}-4 - name: Install OS dependencies if: steps.cache-c-core.outputs.cache-hit != 'true' # Only needed when building the C core From 49e2dbf157e8bc3b3b15dd8e9a3f66736d105fe6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 14:59:11 +0200 Subject: [PATCH 0358/1681] fix: fall back to earlier versions of test dependencies so they support Python 3.6 --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index ced505305..25fce3f4d 100644 --- a/setup.py +++ b/setup.py @@ -827,10 +827,10 @@ def use_educated_guess(self): extras_require={ "plotting": ["cairocffi>=1.2.0"], "test": [ - "networkx>=2.6.2", - "numpy>=1.21.2", - "pandas>=1.3.2", - "scipy>=1.7.1", + "networkx>=2.5", + "numpy>=1.19.0", + "pandas>=1.1.0", + "scipy>=1.5.0", "pytest>=6.2.5", ], }, From 8d4a3cf2ca6ad4622559323806fde72793cf3794 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 15:18:22 +0200 Subject: [PATCH 0359/1681] chore: updated vendored igraph to fix a compilation problem with PRId32 / PRId64 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index fb17862e3..6f154c7ac 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit fb17862e3c8096171449b1680581a3b1e55e8a29 +Subproject commit 6f154c7ac0946f7a0baf1ce403c520cacf6035d7 From 6fad0780bb33c7d550296c3839a2cff828819b9c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 15:53:40 +0200 Subject: [PATCH 0360/1681] test: don't attempt to install complicated test dependencies on PyPy --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 25fce3f4d..3f79ddb63 100644 --- a/setup.py +++ b/setup.py @@ -828,10 +828,10 @@ def use_educated_guess(self): "plotting": ["cairocffi>=1.2.0"], "test": [ "networkx>=2.5", - "numpy>=1.19.0", - "pandas>=1.1.0", - "scipy>=1.5.0", "pytest>=6.2.5", + 'numpy>=1.19.0 ; platform_python_implementation != "pypy"', + 'pandas>=1.1.0 ; platform_python_implementation != "pypy"', + 'scipy>=1.5.0 ; platform_python_implementation != "pypy"', ], }, headers=headers, From 75db285ce2d521788ac034961b0d4ddb58c5c4f4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 21:34:50 +0200 Subject: [PATCH 0361/1681] chore: make it clear in setup.py that we don't support Python <3.6 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 3f79ddb63..ac341ad21 100644 --- a/setup.py +++ b/setup.py @@ -834,6 +834,7 @@ def use_educated_guess(self): 'scipy>=1.5.0 ; platform_python_implementation != "pypy"', ], }, + python_requires=">=3.6", headers=headers, platforms="ALL", keywords=[ From 0ce1d508be3f420fb014ca404224dc70f44194b2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 21:36:56 +0200 Subject: [PATCH 0362/1681] fix: fix PyPy detection in test dependencies in setup.py --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index ac341ad21..573ef7ae6 100644 --- a/setup.py +++ b/setup.py @@ -829,9 +829,9 @@ def use_educated_guess(self): "test": [ "networkx>=2.5", "pytest>=6.2.5", - 'numpy>=1.19.0 ; platform_python_implementation != "pypy"', - 'pandas>=1.1.0 ; platform_python_implementation != "pypy"', - 'scipy>=1.5.0 ; platform_python_implementation != "pypy"', + 'numpy>=1.19.0 ; platform_python_implementation != "PyPy"', + 'pandas>=1.1.0 ; platform_python_implementation != "PyPy"', + 'scipy>=1.5.0 ; platform_python_implementation != "PyPy"', ], }, python_requires=">=3.6", From ae8bc40e9ecba0e2cca2e4b063b32eaaa3867114 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 3 Sep 2021 23:33:34 +0200 Subject: [PATCH 0363/1681] fix: trying alternative format for PyPy detection in setup.py extras --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 573ef7ae6..b97a8904a 100644 --- a/setup.py +++ b/setup.py @@ -829,9 +829,9 @@ def use_educated_guess(self): "test": [ "networkx>=2.5", "pytest>=6.2.5", - 'numpy>=1.19.0 ; platform_python_implementation != "PyPy"', - 'pandas>=1.1.0 ; platform_python_implementation != "PyPy"', - 'scipy>=1.5.0 ; platform_python_implementation != "PyPy"', + "numpy>=1.19.0; platform_python_implementation != 'PyPy'", + "pandas>=1.1.0; platform_python_implementation != 'PyPy'", + "scipy>=1.5.0; platform_python_implementation != 'PyPy'", ], }, python_requires=">=3.6", From 3f65848ff5e40a1e9496c7a977757513bc362c55 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 4 Sep 2021 00:30:12 +0200 Subject: [PATCH 0364/1681] fix: restrict Pandas version in unit tests to ensure that we have wheels for all CPython versions --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b97a8904a..3ed791065 100644 --- a/setup.py +++ b/setup.py @@ -830,7 +830,7 @@ def use_educated_guess(self): "networkx>=2.5", "pytest>=6.2.5", "numpy>=1.19.0; platform_python_implementation != 'PyPy'", - "pandas>=1.1.0; platform_python_implementation != 'PyPy'", + "pandas>=1.1.0,<1.3.1; platform_python_implementation != 'PyPy'", "scipy>=1.5.0; platform_python_implementation != 'PyPy'", ], }, From 98d6b47edd500d16e78fbcafd7649b00712ed750 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 4 Sep 2021 10:13:45 +0200 Subject: [PATCH 0365/1681] fix: numpy_to_contiguous_memoryview() now works for 64-bit and 32-bit igraph as well --- src/_igraph/igraphmodule.c | 2 ++ src/igraph/utils.py | 11 ++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 31946354e..e8639c366 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -904,6 +904,8 @@ PyObject* PyInit__igraph(void) PyModule_AddIntConstant(m, "LOOPS_SW", IGRAPH_LOOPS_SW); PyModule_AddIntConstant(m, "MULTI_SW", IGRAPH_MULTI_SW); + PyModule_AddIntConstant(m, "INTEGER_SIZE", IGRAPH_INTEGER_SIZE); + /* More useful constants */ { const char* version; diff --git a/src/igraph/utils.py b/src/igraph/utils.py index 8cc381cb4..e1bce6541 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -60,19 +60,20 @@ def numpy_to_contiguous_memoryview(obj): """ # Deferred import to prevent a hard dependency on NumPy from numpy import int32, int64, require + from igraph._igraph import INTEGER_SIZE # TODO: we used to export to double, which is only dependent on the # architecture. Now with integers and a compile-time flag, we have # to figure out what is the integer bitness of the underlying C core. # Think of how to do that, for now default to 64 bit ints! - size = 8 - #size = sizeof(c_double) - if size == 8: + if INTEGER_SIZE == 64: dtype = int64 - elif size == 4: + elif INTEGER_SIZE == 32: dtype = int32 else: - raise TypeError("size of C double (%d bytes) is not supported" % size) + raise TypeError( + f"size of igraph_integer_t in the C layer ({INTEGER_SIZE} bits) is not supported" + ) return memoryview(require(obj, dtype=dtype, requirements="AC")) From 8a796015c0ec1ebfb480e509f590b02f62e2efc6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 4 Sep 2021 10:19:18 +0200 Subject: [PATCH 0366/1681] fix: conversion from memoryview to edge list needs to rely on the size of igraph_integer_t now --- src/_igraph/convert.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index ad261fccf..f50fb313a 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1516,9 +1516,9 @@ int igraphmodule_PyObject_to_edgelist( if (PyMemoryView_Check(list)) { buffer = PyMemoryView_GET_BUFFER(list); - if (buffer->itemsize != sizeof(igraph_real_t)) { + if (buffer->itemsize != sizeof(igraph_integer_t)) { PyErr_SetString( - PyExc_TypeError, "item size of buffer must match the size of igraph_real_t" + PyExc_TypeError, "item size of buffer must match the size of igraph_integer_t" ); return 1; } From 426b43d715917138e2d835550fe7bb4adef1e3a3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 4 Sep 2021 23:30:46 +0200 Subject: [PATCH 0367/1681] fix: conversion improvements, slowly getting rid of (long) casts --- src/_igraph/convert.c | 218 +++++++++++++++++++++++++------------- src/_igraph/convert.h | 8 +- src/_igraph/graphobject.c | 2 +- 3 files changed, 150 insertions(+), 78 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index f50fb313a..46080a8d0 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1258,6 +1258,48 @@ int igraphmodule_PyObject_to_vector_bool_t(PyObject *list, return 0; } +/** + * \ingroup python_interface_conversion + * \brief Converts an igraph \c igraph_integer_t to a Python integer + * + * \param value the \c igraph_integer_t value to be converted + * \return the Python integer as a \c PyObject*, or \c NULL if an + * error occurred + */ +PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value) { +#if IGRAPH_INTEGER_SIZE == 32 + /* minimum size of a long is 32 bits so we are okay */ + return PyLong_FromLong(value); +#elif IGRAPH_INTEGER_SIZE == 64 + /* minimum size of a long long is 64 bits so we are okay */ + return PyLong_FromLongLong(value); +#else +# error "Unknown igraph_integer_t size" +#endif +} + +/** + * \ingroup python_interface_conversion + * \brief Converts an igraph \c igraph_real_t to a Python float or integer + * + * \param value the \c igraph_real_t value to be converted + * \return the Python float or integer as a \c PyObject*, or \c NULL if an + * error occurred + */ +PyObject* igraphmodule_real_t_to_PyObject(igraph_real_t value, igraphmodule_conv_t type) { + if (type == IGRAPHMODULE_TYPE_INT) { + if (!igraph_finite(value)) { + return PyFloat_FromDouble(value); + } else { + return igraphmodule_integer_t_to_PyObject((igraph_integer_t)value); + } + } else if (type == IGRAPHMODULE_TYPE_FLOAT) { + return PyFloat_FromDouble(value); + } else { + Py_RETURN_NONE; + } +} + /** * \ingroup python_interface_conversion * \brief Converts an igraph \c igraph_vector_bool_t to a Python boolean list @@ -1291,28 +1333,22 @@ PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v) { * \param v the \c igraph_vector_t containing the vector to be converted * \return the Python integer list as a \c PyObject*, or \c NULL if an error occurred */ -PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, - igraphmodule_conv_t type) { +PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, igraphmodule_conv_t type) { PyObject *list, *item; Py_ssize_t n, i; - n=igraph_vector_size(v); - if (n<0) return igraphmodule_handle_igraph_error(); + n = igraph_vector_size(v); + if (n < 0) { + return igraphmodule_handle_igraph_error(); + } - list=PyList_New(n); - for (i=0; i Date: Sun, 5 Sep 2021 14:46:23 +0200 Subject: [PATCH 0368/1681] fix: more conversion improvements --- src/_igraph/convert.c | 258 ++++++++++++++++++++++++++---------------- 1 file changed, 161 insertions(+), 97 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 46080a8d0..b51aa2211 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1312,12 +1312,17 @@ PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v) { PyObject *list, *item; Py_ssize_t n, i; - n=igraph_vector_bool_size(v); - if (n<0) + n = igraph_vector_bool_size(v); + if (n < 0) { return igraphmodule_handle_igraph_error(); + } - list=PyList_New(n); - for (i=0; i>=1; - list=PyList_New(n); - - /* populate the list with data */ - for (i=0, j=0; i>= 1; + list = PyList_New(n); + if (!list) { + return NULL; + } + + /* populate the list with data */ + for (i = 0, j = 0; i < n; i++, j += 2) { + first = igraphmodule_integer_t_to_PyObject(VECTOR(*v)[j]); + if (!first) { + Py_DECREF(list); + return NULL; + } + + second = igraphmodule_integer_t_to_PyObject(VECTOR(*v)[j + 1]); + if (!second) { + Py_DECREF(first); + Py_DECREF(list); + return NULL; + } + + pair = PyTuple_Pack(2, first, second); + if (pair == NULL) { + Py_DECREF(second); + Py_DECREF(first); + Py_DECREF(list); + return NULL; + } - return list; + Py_DECREF(first); + Py_DECREF(second); + first = second = 0; + + PyList_SET_ITEM(list, i, pair); + } + + return list; } /** @@ -1982,7 +2010,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * /** * \ingroup python_interface_conversion - * \brief Converts two igraph \c igraph_vector_t vectors to a Python list of integer pairs + * \brief Converts two igraph \c igraph_vector_int_t vectors to a Python list of integer pairs * * \param v1 the \c igraph_vector_int_t containing the 1st vector to be converted * \param v2 the \c igraph_vector_int_t containing the 2nd vector to be converted @@ -1990,28 +2018,51 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * */ PyObject* igraphmodule_vector_int_t_pair_to_PyList(const igraph_vector_int_t *v1, const igraph_vector_int_t *v2) { - PyObject *list, *pair; - long n, i; - - n=igraph_vector_int_size(v1); - if (n<0) return igraphmodule_handle_igraph_error(); - if (igraph_vector_int_size(v2) != n) return igraphmodule_handle_igraph_error(); - - /* create a new Python list */ - list=PyList_New(n); - - /* populate the list with data */ - for (i=0; i Date: Sun, 5 Sep 2021 16:16:39 +0200 Subject: [PATCH 0369/1681] refactor: edgeobject.* is now free of unnecessary long int casts --- src/_igraph/convert.c | 16 +++++--- src/_igraph/edgeobject.c | 77 ++++++++++++++++++++++--------------- src/_igraph/edgeobject.h | 3 +- src/_igraph/edgeseqobject.c | 2 +- 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index b51aa2211..c84f66dfa 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2927,7 +2927,7 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g } else if (PyObject_IsInstance(o, (PyObject*)&igraphmodule_EdgeType)) { /* Single edge ID from Edge object */ igraphmodule_EdgeObject *eo = (igraphmodule_EdgeObject*)o; - *eid = igraphmodule_Edge_get_index_igraph_integer(eo); + *eid = igraphmodule_Edge_get_index_as_igraph_integer(eo); } else if (PyIndex_Check(o)) { /* Other numeric type that can be converted to an index */ PyObject* num = PyNumber_Index(o); @@ -2965,8 +2965,11 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g retval = igraph_get_eid(graph, eid, vid1, vid2, 1, 0); if (retval == IGRAPH_EINVVID) { - PyErr_Format(PyExc_ValueError, "no edge from vertex #%ld to #%ld; no such vertex ID", - (long int)vid1, (long int)vid2); + PyErr_Format( + PyExc_ValueError, + "no edge from vertex #%" IGRAPH_PRId " to #%" IGRAPH_PRId "; no such vertex ID", + vid1, vid2 + ); return 1; } else if (retval) { igraphmodule_handle_igraph_error(); @@ -2974,8 +2977,11 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g } if (*eid < 0) { - PyErr_Format(PyExc_ValueError, "no edge from vertex #%ld to #%ld", - (long int)vid1, (long int)vid2); + PyErr_Format( + PyExc_ValueError, + "no edge from vertex #%" IGRAPH_PRId " to #%" IGRAPH_PRId, + vid1, vid2 + ); return 1; } } else { diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 6f3010ee4..491ebe1cb 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -23,6 +23,7 @@ */ #include "attributes.h" +#include "convert.h" #include "edgeobject.h" #include "error.h" #include "graphobject.h" @@ -150,8 +151,8 @@ PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self) { if (attrs == 0) return NULL; - s = PyUnicode_FromFormat("igraph.Edge(%R, %ld, %R)", - (PyObject*)self->gref, (long int)self->idx, attrs); + s = PyUnicode_FromFormat("igraph.Edge(%R, %" IGRAPH_PRId ", %R)", + (PyObject*)self->gref, self->idx, attrs); Py_DECREF(attrs); return s; @@ -169,7 +170,7 @@ long igraphmodule_Edge_hash(igraphmodule_EdgeObject* self) { if (self->hash != -1) return self->hash; - index_o = PyLong_FromLong((long int)self->idx); + index_o = igraphmodule_integer_t_to_PyObject(self->idx); if (index_o == 0) return -1; @@ -260,23 +261,25 @@ PyObject* igraphmodule_Edge_attribute_names(igraphmodule_EdgeObject* self) { PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self) { igraphmodule_GraphObject *o = self->gref; PyObject *names, *dict; - long int i, n; + Py_ssize_t i, n; - if (!igraphmodule_Edge_Validate((PyObject*)self)) - return 0; + if (!igraphmodule_Edge_Validate((PyObject*)self)) { + return NULL; + } - dict=PyDict_New(); - if (!dict) + dict = PyDict_New(); + if (!dict) { return NULL; + } - names=igraphmodule_Graph_edge_attributes(o); + names = igraphmodule_Graph_edge_attributes(o); if (!names) { Py_DECREF(dict); return NULL; } n = PyList_Size(names); - for (i=0; igref; igraph_integer_t from, to; - if (!igraphmodule_Edge_Validate((PyObject*)self)) + if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; + } if (igraph_edge(&o->g, self->idx, &from, &to)) { - igraphmodule_handle_igraph_error(); return NULL; + return igraphmodule_handle_igraph_error(); } - return PyLong_FromLong((long int)from); + + return igraphmodule_integer_t_to_PyObject(from); } /** @@ -458,13 +463,15 @@ PyObject* igraphmodule_Edge_get_to(igraphmodule_EdgeObject* self, void* closure) igraphmodule_GraphObject *o = self->gref; igraph_integer_t from, to; - if (!igraphmodule_Edge_Validate((PyObject*)self)) + if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; + } if (igraph_edge(&o->g, self->idx, &from, &to)) { - igraphmodule_handle_igraph_error(); return NULL; + return igraphmodule_handle_igraph_error(); } - return PyLong_FromLong((long)to); + + return igraphmodule_integer_t_to_PyObject(to); } /** @@ -490,25 +497,17 @@ PyObject* igraphmodule_Edge_get_target_vertex(igraphmodule_EdgeObject* self, voi * Returns the edge index */ PyObject* igraphmodule_Edge_get_index(igraphmodule_EdgeObject* self, void* closure) { - return PyLong_FromLong((long int)self->idx); + return igraphmodule_integer_t_to_PyObject(self->idx); } /** * \ingroup python_interface_edge * Returns the edge index as an igraph_integer_t */ -igraph_integer_t igraphmodule_Edge_get_index_igraph_integer(igraphmodule_EdgeObject* self) { +igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self) { return self->idx; } -/** - * \ingroup python_interface_edge - * Returns the edge index as an ordinary C long - */ -long igraphmodule_Edge_get_index_long(igraphmodule_EdgeObject* self) { - return (long)self->idx; -} - /** * \ingroup python_interface_edge * Returns the source and target vertex index of an edge @@ -516,14 +515,32 @@ long igraphmodule_Edge_get_index_long(igraphmodule_EdgeObject* self) { PyObject* igraphmodule_Edge_get_tuple(igraphmodule_EdgeObject* self, void* closure) { igraphmodule_GraphObject *o = self->gref; igraph_integer_t from, to; + PyObject *from_o, *to_o, *result; - if (!igraphmodule_Edge_Validate((PyObject*)self)) + if (!igraphmodule_Edge_Validate((PyObject*)self)) { return NULL; + } if (igraph_edge(&o->g, self->idx, &from, &to)) { - igraphmodule_handle_igraph_error(); return NULL; + return igraphmodule_handle_igraph_error(); + } + + from_o = igraphmodule_integer_t_to_PyObject(from); + if (!from_o) { + return NULL; } - return Py_BuildValue("(ii)", (long)from, (long)to); + + to_o = igraphmodule_integer_t_to_PyObject(to); + if (!to_o) { + Py_DECREF(from_o); + return NULL; + } + + result = PyTuple_Pack(2, from_o, to_o); + Py_DECREF(to_o); + Py_DECREF(from_o); + + return result; } /** @@ -567,7 +584,7 @@ PyObject* igraphmodule_Edge_get_graph(igraphmodule_EdgeObject* self, void* closu #define GRAPH_PROXY_METHOD(FUNC, METHODNAME) \ PyObject* igraphmodule_Edge_##FUNC(igraphmodule_EdgeObject* self, PyObject* args, PyObject* kwds) { \ PyObject *new_args, *item, *result; \ - long int i, num_args = args ? PyTuple_Size(args)+1 : 1; \ + Py_ssize_t i, num_args = args ? PyTuple_Size(args)+1 : 1; \ \ /* Prepend ourselves to args */ \ new_args = PyTuple_New(num_args); \ diff --git a/src/_igraph/edgeobject.h b/src/_igraph/edgeobject.h index 2d7f3a259..3c0f146f3 100644 --- a/src/_igraph/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -47,8 +47,7 @@ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self); PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self); PyObject* igraphmodule_Edge_attribute_names(igraphmodule_EdgeObject* self); -igraph_integer_t igraphmodule_Edge_get_index_igraph_integer(igraphmodule_EdgeObject* self); -long igraphmodule_Edge_get_index_long(igraphmodule_EdgeObject* self); +igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); PyObject* igraphmodule_Edge_update_attributes(PyObject* self, PyObject* args, PyObject* kwds); diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 1e6c58bc0..bb575b461 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -612,7 +612,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject } if (PyObject_IsTrue(call_result)) igraph_vector_int_push_back(&v, - igraphmodule_Edge_get_index_long((igraphmodule_EdgeObject*)edge)); + igraphmodule_Edge_get_index_as_igraph_integer((igraphmodule_EdgeObject*)edge)); else was_excluded=1; Py_DECREF(call_result); Py_DECREF(edge); From 66662b6f99c849fcf23956a06d2d59c62f6a6edf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 5 Sep 2021 22:12:40 +0200 Subject: [PATCH 0370/1681] refactor: removed some unnecessary casts from edgeseqobject.c --- src/_igraph/edgeseqobject.c | 157 ++++++++++++++++++++++------------ src/_igraph/edgeseqobject.h | 2 +- src/_igraph/vertexseqobject.c | 13 ++- src/_igraph/vertexseqobject.h | 2 +- 4 files changed, 114 insertions(+), 60 deletions(-) diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index bb575b461..86de684ed 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -117,12 +117,18 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, igraph_es_all(&es, IGRAPH_EDGEORDER_ID); } else if (PyLong_Check(esobj)) { /* We selected a single edge */ - long int idx = PyLong_AsLong(esobj); + igraph_integer_t idx; + + if (igraphmodule_PyObject_to_integer_t(esobj, &idx)) { + return -1; + } + if (idx < 0 || idx >= igraph_ecount(&((igraphmodule_GraphObject*)g)->g)) { PyErr_SetString(PyExc_ValueError, "edge index out of range"); return -1; } - igraph_es_1(&es, (igraph_integer_t)idx); + + igraph_es_1(&es, idx); } else { /* We selected multiple edges */ igraph_vector_int_t v; @@ -170,7 +176,7 @@ void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self) { * \ingroup python_interface_edgeseq * \brief Returns the length of the sequence (i.e. the number of edges in the graph) */ -int igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { +Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { igraph_t *g; igraph_integer_t result; @@ -179,7 +185,7 @@ int igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { igraphmodule_handle_igraph_error(); return -1; } - return (int)result; + return result; } /** @@ -199,7 +205,7 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, i = igraph_ecount(g) + i; } if (i >= 0 && i < igraph_ecount(g)) { - idx = (igraph_integer_t)i; + idx = i; } break; @@ -209,7 +215,7 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, i = igraph_vector_int_size(self->es.data.vecptr) + i; } if (i >= 0 && i < igraph_vector_int_size(self->es.data.vecptr)) { - idx = (igraph_integer_t)VECTOR(*self->es.data.vecptr)[i]; + idx = VECTOR(*self->es.data.vecptr)[i]; } break; @@ -224,7 +230,7 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, i = self->es.data.seq.to - self->es.data.seq.from + i; } if (i >= 0 && i < self->es.data.seq.to - self->es.data.seq.from) { - idx = self->es.data.seq.from + (igraph_integer_t)i; + idx = self->es.data.seq.from + i; } break; @@ -261,33 +267,40 @@ PyObject* igraphmodule_EdgeSeq_attribute_names(igraphmodule_EdgeSeqObject* self) PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* self, PyObject* o) { igraphmodule_GraphObject *gr = self->gref; PyObject *result=0, *values, *item; - long int i, n; + Py_ssize_t i, n; if (!igraphmodule_attribute_name_check(o)) return 0; PyErr_Clear(); - values=PyDict_GetItem(ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE], o); + values = PyDict_GetItem(ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE], o); if (!values) { PyErr_SetString(PyExc_KeyError, "Attribute does not exist"); return NULL; - } else if (PyErr_Occurred()) return NULL; + } else if (PyErr_Occurred()) { + return NULL; + } switch (igraph_es_type(&self->es)) { case IGRAPH_ES_NONE: n = 0; result = PyList_New(0); + if (!result) { + return 0; + } break; case IGRAPH_ES_ALL: n = PyList_Size(values); result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } - for (i=0; ies.data.vecptr); result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } - for (i=0; ies.data.vecptr)[i]); + for (i = 0; i < n; i++) { + item = PyList_GET_ITEM(values, VECTOR(*self->es.data.vecptr)[i]); Py_INCREF(item); PyList_SET_ITEM(result, i, item); } @@ -307,10 +322,12 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* case IGRAPH_ES_SEQ: n = self->es.data.seq.to - self->es.data.seq.from; result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } - for (i=0; ies.data.seq.from+i); + for (i = 0; i < n; i++) { + item = PyList_GET_ITEM(values, self->es.data.seq.from + i); Py_INCREF(item); PyList_SET_ITEM(result, i, item); } @@ -364,7 +381,8 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject PyObject *dict, *list, *item; igraphmodule_GraphObject *gr; igraph_vector_int_t es; - long i, j, n, no_of_edges; + Py_ssize_t i, j, n; + igraph_integer_t no_of_edges; gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE]; @@ -384,7 +402,9 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject * single element (the value itself) and then call ourselves again */ int result; PyObject *newList = PyList_New(1); - if (newList == 0) return -1; + if (newList == 0) { + return -1; + } Py_INCREF(values); PyList_SET_ITEM(newList, 0, values); /* reference stolen here */ result = igraphmodule_EdgeSeq_set_attribute_values_mapping(self, attrname, newList); @@ -392,11 +412,13 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject return result; } - n=PySequence_Size(values); - if (n<0) return -1; + n = PySequence_Size(values); + if (n < 0) { + return -1; + } if (igraph_es_type(&self->es) == IGRAPH_ES_ALL) { - no_of_edges = (long)igraph_ecount(&gr->g); + no_of_edges = igraph_ecount(&gr->g); if (n == 0 && no_of_edges > 0) { PyErr_SetString(PyExc_ValueError, "sequence must not be empty"); return -1; @@ -406,10 +428,14 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject list = PyDict_GetItem(dict, attrname); if (list != 0) { /* Yes, we have. Modify its items to the items found in values */ - for (i=0, j=0; i 0) { PyErr_SetString(PyExc_ValueError, "sequence must not be empty"); igraph_vector_int_destroy(&es); @@ -457,12 +489,16 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject list = PyDict_GetItem(dict, attrname); if (list != 0) { /* Yes, we have. Modify its items to the items found in values */ - for (i=0, j=0; ig); + igraph_integer_t n2 = igraph_ecount(&gr->g); list = PyList_New(n2); - if (list == 0) { igraph_vector_int_destroy(&es); return -1; } - for (i=0; igref; result=igraphmodule_EdgeSeq_copy(self); @@ -576,7 +621,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject /* First, filter by positional arguments */ n = PyTuple_Size(args); - for (i=0; i= m || idx < 0) { + Py_DECREF(result); PyErr_SetString(PyExc_ValueError, "edge index out of range"); igraph_vector_int_destroy(&v); igraph_vector_int_destroy(&v2); @@ -748,7 +797,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject return 0; } /* Do the iteration */ - while ((item2=PyIter_Next(iter)) != 0) { + while ((item2 = PyIter_Next(iter)) != 0) { if (igraphmodule_PyObject_to_integer_t(item2, &igraph_idx)) { /* We simply ignore elements that we don't know */ Py_DECREF(item2); @@ -762,7 +811,7 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject igraph_vector_int_destroy(&v2); return NULL; } - if (igraph_vector_int_push_back(&v, VECTOR(v2)[(long int) igraph_idx])) { + if (igraph_vector_int_push_back(&v, VECTOR(v2)[igraph_idx])) { Py_DECREF(result); Py_DECREF(iter); igraphmodule_handle_igraph_error(); diff --git a/src/_igraph/edgeseqobject.h b/src/_igraph/edgeseqobject.h index 7a410b9e1..ffca889fc 100644 --- a/src/_igraph/edgeseqobject.h +++ b/src/_igraph/edgeseqobject.h @@ -46,7 +46,7 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, PyObject *kwds); void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self); -int igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject *self); +Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject *self); PyObject* igraphmodule_EdgeSeq_find(igraphmodule_EdgeSeqObject *self, PyObject *args); diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 3c015a3ba..15a826419 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -173,16 +173,21 @@ void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self) { * \ingroup python_interface_vertexseq * \brief Returns the length of the sequence */ -int igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) { +Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) { igraph_t *g; igraph_integer_t result; - if (!self->gref) return -1; - g=&GET_GRAPH(self); + + if (!self->gref) { + return -1; + } + + g = &GET_GRAPH(self); if (igraph_vs_size(g, &self->vs, &result)) { igraphmodule_handle_igraph_error(); return -1; } - return (int)result; + + return result; } /** diff --git a/src/_igraph/vertexseqobject.h b/src/_igraph/vertexseqobject.h index 708889a97..95cbf416b 100644 --- a/src/_igraph/vertexseqobject.h +++ b/src/_igraph/vertexseqobject.h @@ -43,7 +43,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject* self, PyObject* args, PyObject* kwds); void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self); -int igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject *self); +Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject *self); PyObject* igraphmodule_VertexSeq_find(igraphmodule_VertexSeqObject *self, PyObject *args); From 759cd5727953e69bdb2c7ebae32bf407b7336694 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 5 Sep 2021 23:06:05 +0200 Subject: [PATCH 0371/1681] refactor: BFSIter now uses dqueue_int --- src/_igraph/bfsiter.c | 70 ++++++++++++++++++++++++------------------- src/_igraph/bfsiter.h | 2 +- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index c86c90702..2108b19a1 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -22,6 +22,7 @@ #include "bfsiter.h" #include "common.h" +#include "convert.h" #include "error.h" #include "vertexobject.h" @@ -42,9 +43,9 @@ PyTypeObject igraphmodule_BFSIterType; */ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { igraphmodule_BFSIterObject* o; - long int no_of_nodes, r; + igraph_integer_t no_of_nodes, r; - o=PyObject_GC_New(igraphmodule_BFSIterObject, &igraphmodule_BFSIterType); + o = PyObject_GC_New(igraphmodule_BFSIterObject, &igraphmodule_BFSIterType); Py_INCREF(g); o->gref=g; o->graph=&g->g; @@ -54,39 +55,47 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, return NULL; } - no_of_nodes=igraph_vcount(&g->g); + no_of_nodes = igraph_vcount(&g->g); o->visited=(char*)calloc(no_of_nodes, sizeof(char)); if (o->visited == 0) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_dqueue_init(&o->queue, 100)) { + if (igraph_dqueue_int_init(&o->queue, 100)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } + if (igraph_vector_int_init(&o->neis, 0)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); - igraph_dqueue_destroy(&o->queue); + igraph_dqueue_int_destroy(&o->queue); return NULL; } if (PyLong_Check(root)) { - r=PyLong_AsLong(root); + if (igraphmodule_PyObject_to_integer_t(root, &r)) { + igraph_dqueue_int_destroy(&o->queue); + return NULL; + } } else { - r=((igraphmodule_VertexObject*)root)->idx; + r = ((igraphmodule_VertexObject*)root)->idx; } - if (igraph_dqueue_push(&o->queue, r) || - igraph_dqueue_push(&o->queue, 0) || - igraph_dqueue_push(&o->queue, -1)) { - igraph_dqueue_destroy(&o->queue); + + if (igraph_dqueue_int_push(&o->queue, r) || + igraph_dqueue_int_push(&o->queue, 0) || + igraph_dqueue_int_push(&o->queue, -1)) { + igraph_dqueue_int_destroy(&o->queue); igraph_vector_int_destroy(&o->neis); PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } o->visited[r]=1; - if (!igraph_is_directed(&g->g)) mode=IGRAPH_ALL; + if (!igraph_is_directed(&g->g)) { + mode=IGRAPH_ALL; + } + o->mode=mode; o->advanced=advanced; @@ -131,7 +140,7 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { self->gref=NULL; Py_XDECREF(tmp); - igraph_dqueue_destroy(&self->queue); + igraph_dqueue_int_destroy(&self->queue); igraph_vector_int_destroy(&self->neis); free(self->visited); self->visited=0; @@ -157,27 +166,28 @@ PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { } PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { - if (!igraph_dqueue_empty(&self->queue)) { - igraph_integer_t vid = (igraph_integer_t)igraph_dqueue_pop(&self->queue); - igraph_integer_t dist = (igraph_integer_t)igraph_dqueue_pop(&self->queue); - igraph_integer_t parent = (igraph_integer_t)igraph_dqueue_pop(&self->queue); - long int i; + if (!igraph_dqueue_int_empty(&self->queue)) { + igraph_integer_t vid = igraph_dqueue_int_pop(&self->queue); + igraph_integer_t dist = igraph_dqueue_int_pop(&self->queue); + igraph_integer_t parent = igraph_dqueue_int_pop(&self->queue); + igraph_integer_t i, n; if (igraph_neighbors(self->graph, &self->neis, vid, self->mode)) { igraphmodule_handle_igraph_error(); return NULL; } - - for (i=0; ineis); i++) { - igraph_integer_t neighbor = (igraph_integer_t)VECTOR(self->neis)[i]; - if (self->visited[neighbor]==0) { - self->visited[neighbor]=1; - if (igraph_dqueue_push(&self->queue, neighbor) || - igraph_dqueue_push(&self->queue, dist+1) || - igraph_dqueue_push(&self->queue, vid)) { - igraphmodule_handle_igraph_error(); - return NULL; - } + + n = igraph_vector_int_size(&self->neis); + for (i = 0; i < n; i++) { + igraph_integer_t neighbor = VECTOR(self->neis)[i]; + if (self->visited[neighbor] == 0) { + self->visited[neighbor] = 1; + if (igraph_dqueue_int_push(&self->queue, neighbor) || + igraph_dqueue_int_push(&self->queue, dist+1) || + igraph_dqueue_int_push(&self->queue, vid)) { + igraphmodule_handle_igraph_error(); + return NULL; + } } } @@ -194,7 +204,7 @@ PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { Py_INCREF(Py_None); parentobj=Py_None; } - return Py_BuildValue("NlN", vertexobj, (long int)dist, parentobj); + return Py_BuildValue("NnN", vertexobj, (Py_ssize_t)dist, parentobj); } else { return igraphmodule_Vertex_New(self->gref, vid); } diff --git a/src/_igraph/bfsiter.h b/src/_igraph/bfsiter.h index e36f03069..f713ba4ee 100644 --- a/src/_igraph/bfsiter.h +++ b/src/_igraph/bfsiter.h @@ -34,7 +34,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_dqueue_t queue; + igraph_dqueue_int_t queue; igraph_vector_int_t neis; igraph_t *graph; char *visited; From 1d3a2926e87a4284ba09ce415d54b8936db6cc83 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 5 Sep 2021 23:06:20 +0200 Subject: [PATCH 0372/1681] refactor: more type casting cleanup --- src/_igraph/igraphmodule.c | 9 ++++-- src/_igraph/operators.c | 52 +++++++++++++++++++---------------- src/_igraph/vertexobject.c | 10 +------ src/_igraph/vertexobject.h | 1 - src/_igraph/vertexseqobject.c | 4 +-- 5 files changed, 39 insertions(+), 37 deletions(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index e8639c366..9dbd3afa0 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -577,8 +577,10 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, &comm1_o, &comm2_o)) return NULL; - if (igraphmodule_PyObject_to_vector_int_t(comm1_o, &comm1)) + if (igraphmodule_PyObject_to_vector_int_t(comm1_o, &comm1)) { return NULL; + } + if (igraphmodule_PyObject_to_vector_int_t(comm2_o, &comm2)) { igraph_vector_int_destroy(&comm1); return NULL; @@ -590,10 +592,13 @@ PyObject* igraphmodule_split_join_distance(PyObject *self, igraph_vector_int_destroy(&comm2); return NULL; } + igraph_vector_int_destroy(&comm1); igraph_vector_int_destroy(&comm2); - return Py_BuildValue("ll", (long)distance12, (long)distance21); + /* sizeof(Py_ssize_t) is most likely the same as sizeof(igraph_integer_t), + * but even if it isn't, we cast explicitly so we are safe */ + return Py_BuildValue("nn", (Py_ssize_t)distance12, (Py_ssize_t)distance21); } /** \ingroup python_interface diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c index 7d8db55bb..0ad5a0293 100644 --- a/src/_igraph/operators.c +++ b/src/_igraph/operators.c @@ -34,7 +34,7 @@ PyObject *igraphmodule__disjoint_union(PyObject *self, { static char* kwlist[] = { "graphs", NULL }; PyObject *it, *graphs; - long int no_of_graphs; + Py_ssize_t no_of_graphs; igraph_vector_ptr_t gs; PyObject *result; PyTypeObject *result_type; @@ -62,7 +62,7 @@ PyObject *igraphmodule__disjoint_union(PyObject *self, return NULL; } Py_DECREF(it); - no_of_graphs = (long int) igraph_vector_ptr_size(&gs); + no_of_graphs = igraph_vector_ptr_size(&gs); /* Create disjoint union */ if (igraph_disjoint_union_many(&g, &gs)) { @@ -98,7 +98,7 @@ PyObject *igraphmodule__union(PyObject *self, static char* kwlist[] = { "graphs", "edgemaps", NULL }; PyObject *it, *em_list = 0, *graphs, *with_edgemaps_o; int with_edgemaps = 0; - long int no_of_graphs; + Py_ssize_t i, j, no_of_graphs; igraph_vector_ptr_t gs; igraphmodule_GraphObject *o; PyObject *result; @@ -130,7 +130,8 @@ PyObject *igraphmodule__union(PyObject *self, return NULL; } Py_DECREF(it); - no_of_graphs = (long int) igraph_vector_ptr_size(&gs); + + no_of_graphs = igraph_vector_ptr_size(&gs); if (with_edgemaps) { /* prepare edgemaps */ @@ -148,22 +149,24 @@ PyObject *igraphmodule__union(PyObject *self, } /* extract edgemaps */ - long int i; - em_list = PyList_New((Py_ssize_t) no_of_graphs); + em_list = PyList_New(no_of_graphs); for (i = 0; i < no_of_graphs; i++) { - long int j; - long int no_of_edges = (long int) igraph_ecount(VECTOR(gs)[i]); + Py_ssize_t no_of_edges = igraph_ecount(VECTOR(gs)[i]); igraph_vector_int_t *map = VECTOR(edgemaps)[i]; - PyObject *emi = PyList_New((Py_ssize_t) no_of_edges); + PyObject *emi = PyList_New(no_of_edges); for (j = 0; j < no_of_edges; j++) { - PyObject *dest = PyLong_FromLong((long)VECTOR(*map)[j]); - PyList_SET_ITEM(emi, (Py_ssize_t) j, dest); + PyObject *dest = igraphmodule_integer_t_to_PyObject(VECTOR(*map)[j]); + if (!dest) { + Py_DECREF(emi); + Py_DECREF(em_list); + return NULL; + } + PyList_SET_ITEM(emi, j, dest); } - PyList_SET_ITEM(em_list, (Py_ssize_t) i, emi); + PyList_SET_ITEM(em_list, i, emi); } igraph_vector_ptr_destroy(&edgemaps); - } - else { + } else { /* Create union */ if (igraph_union_many(&g, &gs, /* edgemaps */ 0)) { igraph_vector_ptr_destroy(&gs); @@ -209,7 +212,7 @@ PyObject *igraphmodule__intersection(PyObject *self, static char* kwlist[] = { "graphs", "edgemaps", NULL }; PyObject *it, *em_list = 0, *graphs, *with_edgemaps_o; int with_edgemaps = 0; - long int no_of_graphs; + Py_ssize_t i, j, no_of_graphs; igraph_vector_ptr_t gs; igraphmodule_GraphObject *o; PyObject *result; @@ -241,7 +244,7 @@ PyObject *igraphmodule__intersection(PyObject *self, return NULL; } Py_DECREF(it); - no_of_graphs = (long int) igraph_vector_ptr_size(&gs); + no_of_graphs = igraph_vector_ptr_size(&gs); if (with_edgemaps) { /* prepare edgemaps */ @@ -258,18 +261,21 @@ PyObject *igraphmodule__intersection(PyObject *self, return NULL; } - long int i; em_list = PyList_New((Py_ssize_t) no_of_graphs); for (i = 0; i < no_of_graphs; i++) { - long int j; - long int no_of_edges = (long int) igraph_ecount(VECTOR(gs)[i]); + Py_ssize_t no_of_edges = igraph_ecount(VECTOR(gs)[i]); igraph_vector_int_t *map = VECTOR(edgemaps)[i]; - PyObject *emi = PyList_New((Py_ssize_t) no_of_edges); + PyObject *emi = PyList_New(no_of_edges); for (j = 0; j < no_of_edges; j++) { - PyObject *dest = PyLong_FromLong((long)VECTOR(*map)[j]); - PyList_SET_ITEM(emi, (Py_ssize_t) j, dest); + PyObject *dest = igraphmodule_integer_t_to_PyObject(VECTOR(*map)[j]); + if (!dest) { + Py_DECREF(emi); + Py_DECREF(em_list); + return NULL; + } + PyList_SET_ITEM(emi, j, dest); } - PyList_SET_ITEM(em_list, (Py_ssize_t) i, emi); + PyList_SET_ITEM(em_list, i, emi); } igraph_vector_ptr_destroy(&edgemaps); diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 287ecba0e..bcef9561e 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -550,7 +550,7 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* * Returns the vertex index */ PyObject* igraphmodule_Vertex_get_index(igraphmodule_VertexObject* self, void* closure) { - return PyLong_FromLong((long int)self->idx); + return igraphmodule_integer_t_to_PyObject(self->idx); } /** @@ -561,14 +561,6 @@ igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_Verte return self->idx; } -/** - * \ingroup python_interface_vertex - * Returns the vertex index as an ordinary C long - */ -long igraphmodule_Vertex_get_index_long(igraphmodule_VertexObject* self) { - return (long)self->idx; -} - /** * \ingroup python_interface_vertexseq * Returns the graph where the vertex belongs diff --git a/src/_igraph/vertexobject.h b/src/_igraph/vertexobject.h index f33d640eb..a50a6adf8 100644 --- a/src/_igraph/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -49,7 +49,6 @@ PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self); PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self); PyObject* igraphmodule_Vertex_attribute_names(igraphmodule_VertexObject* self); igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); -long igraphmodule_Vertex_get_index_long(igraphmodule_VertexObject* self); PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, PyObject* kwds); diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 15a826419..403f01a90 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -186,7 +186,7 @@ Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject* self) igraphmodule_handle_igraph_error(); return -1; } - + return result; } @@ -651,7 +651,7 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, } if (PyObject_IsTrue(call_result)) igraph_vector_int_push_back(&v, - igraphmodule_Vertex_get_index_long((igraphmodule_VertexObject*)vertex)); + igraphmodule_Vertex_get_index_igraph_integer((igraphmodule_VertexObject*)vertex)); else was_excluded=1; Py_DECREF(call_result); Py_DECREF(vertex); From 1bb7f39a26036e668c7bfcc9d48d83ed885455de Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 6 Sep 2021 22:52:39 +0200 Subject: [PATCH 0373/1681] refactor: DFSIter now uses igraph_integer_t properly --- src/_igraph/bfsiter.c | 1 + src/_igraph/dfsiter.c | 101 +++++++++++++++++++++---------------- src/_igraph/dfsiter.h | 2 +- src/_igraph/vertexobject.c | 6 +-- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 2108b19a1..03615ceb9 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -76,6 +76,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, if (PyLong_Check(root)) { if (igraphmodule_PyObject_to_integer_t(root, &r)) { igraph_dqueue_int_destroy(&o->queue); + igraph_vector_int_destroy(&o->neis); return NULL; } } else { diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 97793aca0..dbbf1c546 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -20,6 +20,7 @@ */ +#include "convert.h" #include "dfsiter.h" #include "common.h" #include "error.h" @@ -42,7 +43,7 @@ PyTypeObject igraphmodule_DFSIterType; */ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { igraphmodule_DFSIterObject* o; - long int no_of_nodes, r; + igraph_integer_t no_of_nodes, r; o=PyObject_GC_New(igraphmodule_DFSIterObject, &igraphmodule_DFSIterType); Py_INCREF(g); @@ -54,42 +55,51 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, return NULL; } - no_of_nodes=igraph_vcount(&g->g); - o->visited=(char*)calloc(no_of_nodes, sizeof(char)); + no_of_nodes = igraph_vcount(&g->g); + o->visited = (char*)calloc(no_of_nodes, sizeof(char)); if (o->visited == 0) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_stack_init(&o->stack, 100)) { + if (igraph_stack_int_init(&o->stack, 100)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } + if (igraph_vector_int_init(&o->neis, 0)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); - igraph_stack_destroy(&o->stack); + igraph_stack_int_destroy(&o->stack); return NULL; } if (PyLong_Check(root)) { - r=PyLong_AsLong(root); + if (igraphmodule_PyObject_to_integer_t(root, &r)) { + igraph_stack_int_destroy(&o->stack); + igraph_vector_int_destroy(&o->neis); + return NULL; + } } else { r = ((igraphmodule_VertexObject*)root)->idx; } + /* push the root onto the stack */ - if (igraph_stack_push(&o->stack, r) || - igraph_stack_push(&o->stack, 0) || - igraph_stack_push(&o->stack, -1)) { - igraph_stack_destroy(&o->stack); + if (igraph_stack_int_push(&o->stack, r) || + igraph_stack_int_push(&o->stack, 0) || + igraph_stack_int_push(&o->stack, -1)) { + igraph_stack_int_destroy(&o->stack); igraph_vector_int_destroy(&o->neis); PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } o->visited[r] = 1; - if (!igraph_is_directed(&g->g)) mode=IGRAPH_ALL; - o->mode=mode; - o->advanced=advanced; + if (!igraph_is_directed(&g->g)) { + mode = IGRAPH_ALL; + } + + o->mode = mode; + o->advanced = advanced; PyObject_GC_Track(o); @@ -112,8 +122,10 @@ int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, RC_TRAVERSE("DFSIter", self); if (self->gref) { - vret=visit((PyObject*)self->gref, arg); - if (vret != 0) return vret; + vret = visit((PyObject*)self->gref, arg); + if (vret != 0) { + return vret; + } } return 0; @@ -128,11 +140,11 @@ int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { PyObject_GC_UnTrack(self); - tmp=(PyObject*)self->gref; + tmp = (PyObject*)self->gref; self->gref = NULL; Py_XDECREF(tmp); - igraph_stack_destroy(&self->stack); + igraph_stack_int_destroy(&self->stack); igraph_vector_int_destroy(&self->neis); free(self->visited); self->visited = 0; @@ -164,28 +176,29 @@ PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { igraph_bool_t any = 0; /* nothing on the stack, end of iterator */ - if(igraph_stack_empty(&self->stack)) { + if (igraph_stack_int_empty(&self->stack)) { return NULL; } /* peek at the top element on the stack * because we save three things, pop 3 in inverse order and push them back */ - parent_out = (igraph_integer_t)igraph_stack_pop(&self->stack); - dist_out = (igraph_integer_t)igraph_stack_pop(&self->stack); - vid_out = (igraph_integer_t)igraph_stack_pop(&self->stack); - igraph_stack_push(&self->stack, (long int) vid_out); - igraph_stack_push(&self->stack, (long int) dist_out); - igraph_stack_push(&self->stack, (long int) parent_out); + parent_out = igraph_stack_int_pop(&self->stack); + dist_out = igraph_stack_int_pop(&self->stack); + vid_out = igraph_stack_int_pop(&self->stack); + igraph_stack_int_push(&self->stack, vid_out); + igraph_stack_int_push(&self->stack, dist_out); + igraph_stack_int_push(&self->stack, parent_out); + + /* look for neighbors until we find one or until we have exhausted the graph */ + while (!any && !igraph_stack_int_empty(&self->stack)) { + igraph_integer_t parent = igraph_stack_int_pop(&self->stack); + igraph_integer_t dist = igraph_stack_int_pop(&self->stack); + igraph_integer_t vid = igraph_stack_int_pop(&self->stack); + igraph_stack_int_push(&self->stack, vid); + igraph_stack_int_push(&self->stack, dist); + igraph_stack_int_push(&self->stack, parent); + igraph_integer_t i, n; - /* look for neighbors until you found one or until you have exausted the graph */ - while (!any && !igraph_stack_empty(&self->stack)) { - igraph_integer_t parent = (igraph_integer_t)igraph_stack_pop(&self->stack); - igraph_integer_t dist = (igraph_integer_t)igraph_stack_pop(&self->stack); - igraph_integer_t vid = (igraph_integer_t)igraph_stack_pop(&self->stack); - igraph_stack_push(&self->stack, (long int) vid); - igraph_stack_push(&self->stack, (long int) dist); - igraph_stack_push(&self->stack, (long int) parent); - long int i; /* the values above are returned at at this stage. However, we must * prepare for the next iteration by putting the next unvisited * neighbor onto the stack */ @@ -193,15 +206,17 @@ PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { igraphmodule_handle_igraph_error(); return NULL; } - for (i=0; ineis); i++) { - igraph_integer_t neighbor = (igraph_integer_t)VECTOR(self->neis)[i]; + + n = igraph_vector_int_size(&self->neis); + for (i = 0; i < n; i++) { + igraph_integer_t neighbor = VECTOR(self->neis)[i]; /* new neighbor, push the next item onto the stack */ if (self->visited[neighbor] == 0) { any = 1; self->visited[neighbor]=1; - if (igraph_stack_push(&self->stack, neighbor) || - igraph_stack_push(&self->stack, dist+1) || - igraph_stack_push(&self->stack, vid)) { + if (igraph_stack_int_push(&self->stack, neighbor) || + igraph_stack_int_push(&self->stack, dist+1) || + igraph_stack_int_push(&self->stack, vid)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -210,9 +225,9 @@ PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { } /* no new neighbors, end of subtree */ if (!any) { - igraph_stack_pop(&self->stack); - igraph_stack_pop(&self->stack); - igraph_stack_pop(&self->stack); + igraph_stack_int_pop(&self->stack); + igraph_stack_int_pop(&self->stack); + igraph_stack_int_pop(&self->stack); } } @@ -229,9 +244,9 @@ PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { return NULL; } else { Py_INCREF(Py_None); - parentobj=Py_None; + parentobj = Py_None; } - return Py_BuildValue("NlN", vertexobj, (long int)dist_out, parentobj); + return Py_BuildValue("NnN", vertexobj, (Py_ssize_t) dist_out, parentobj); } else { return vertexobj; } diff --git a/src/_igraph/dfsiter.h b/src/_igraph/dfsiter.h index 757caf798..18236196d 100644 --- a/src/_igraph/dfsiter.h +++ b/src/_igraph/dfsiter.h @@ -34,7 +34,7 @@ typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; - igraph_stack_t stack; + igraph_stack_int_t stack; igraph_vector_int_t neis; igraph_t *graph; char *visited; diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index bcef9561e..9a470d66c 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -151,8 +151,8 @@ PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self) { if (attrs == 0) return NULL; - s = PyUnicode_FromFormat("igraph.Vertex(%R, %ld, %R)", - (PyObject*)self->gref, (long int)self->idx, attrs); + s = PyUnicode_FromFormat("igraph.Vertex(%R, %" IGRAPH_PRId ", %R)", + (PyObject*)self->gref, self->idx, attrs); Py_DECREF(attrs); return s; @@ -170,7 +170,7 @@ long igraphmodule_Vertex_hash(igraphmodule_VertexObject* self) { if (self->hash != -1) return self->hash; - index_o = PyLong_FromLong((long int)self->idx); + index_o = igraphmodule_integer_t_to_PyObject(self->idx); if (index_o == 0) return -1; From 41d649b1f450fae718d82bec8481e367dc0a0a4e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 6 Sep 2021 23:47:10 +0200 Subject: [PATCH 0374/1681] refactor: (long) casts eliminated from graphobject.c --- src/_igraph/convert.c | 12 ++- src/_igraph/convert.h | 7 +- src/_igraph/graphobject.c | 189 +++++++++++++++++++------------------- 3 files changed, 108 insertions(+), 100 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index c84f66dfa..dd4f9e3ef 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1287,14 +1287,20 @@ PyObject* igraphmodule_integer_t_to_PyObject(igraph_integer_t value) { * error occurred */ PyObject* igraphmodule_real_t_to_PyObject(igraph_real_t value, igraphmodule_conv_t type) { + if (!igraph_finite(value)) { + return PyFloat_FromDouble(value); + } + if (type == IGRAPHMODULE_TYPE_INT) { - if (!igraph_finite(value)) { + return igraphmodule_integer_t_to_PyObject((igraph_integer_t)value); + } else if (type == IGRAPHMODULE_TYPE_FLOAT) { + return PyFloat_FromDouble(value); + } else if (type == IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT) { + if (ceil(value) != value) { return PyFloat_FromDouble(value); } else { return igraphmodule_integer_t_to_PyObject((igraph_integer_t)value); } - } else if (type == IGRAPHMODULE_TYPE_FLOAT) { - return PyFloat_FromDouble(value); } else { Py_RETURN_NONE; } diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index f18be5347..12139427a 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -33,8 +33,11 @@ #include #include "graphobject.h" -typedef enum { IGRAPHMODULE_TYPE_INT=0, IGRAPHMODULE_TYPE_FLOAT } -igraphmodule_conv_t; +typedef enum { + IGRAPHMODULE_TYPE_INT = 0, + IGRAPHMODULE_TYPE_FLOAT = 1, + IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT = 2 +} igraphmodule_conv_t; typedef struct { const char* name; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 23d4d7152..fd4c44d86 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -345,14 +345,17 @@ PyObject* igraphmodule_Graph_from_igraph_t(igraph_t *graph) { */ PyObject *igraphmodule_Graph_str(igraphmodule_GraphObject * self) { - if (igraph_is_directed(&self->g)) - return PyUnicode_FromFormat("Directed graph (|V| = %ld, |E| = %ld)", - (long)igraph_vcount(&self->g), - (long)igraph_ecount(&self->g)); - else - return PyUnicode_FromFormat("Undirected graph (|V| = %ld, |E| = %ld)", - (long)igraph_vcount(&self->g), - (long)igraph_ecount(&self->g)); + if (igraph_is_directed(&self->g)) { + return PyUnicode_FromFormat( + "Directed graph (|V| = %" IGRAPH_PRId ", |E| = %" IGRAPH_PRId ")", + igraph_vcount(&self->g), igraph_ecount(&self->g) + ); + } else { + return PyUnicode_FromFormat( + "Undirected graph (|V| = %" IGRAPH_PRId ", |E| = %" IGRAPH_PRId ")", + igraph_vcount(&self->g), igraph_ecount(&self->g) + ); + } } /** \ingroup python_interface_copy @@ -385,9 +388,7 @@ PyObject *igraphmodule_Graph_copy(igraphmodule_GraphObject * self) */ PyObject *igraphmodule_Graph_vcount(igraphmodule_GraphObject * self) { - PyObject *result; - result = Py_BuildValue("l", (long)igraph_vcount(&self->g)); - return result; + return igraphmodule_integer_t_to_PyObject(igraph_vcount(&self->g)); } /** \ingroup python_interface_graph @@ -397,9 +398,7 @@ PyObject *igraphmodule_Graph_vcount(igraphmodule_GraphObject * self) */ PyObject *igraphmodule_Graph_ecount(igraphmodule_GraphObject * self) { - PyObject *result; - result = Py_BuildValue("l", (long)igraph_ecount(&self->g)); - return result; + return igraphmodule_integer_t_to_PyObject(igraph_ecount(&self->g)); } /** \ingroup python_interface_graph @@ -985,7 +984,7 @@ PyObject *igraphmodule_Graph_maxdegree(igraphmodule_GraphObject * self, igraph_vs_destroy(&vs); - return PyLong_FromLong((long)result); + return igraphmodule_integer_t_to_PyObject(result); } /** \ingroup python_interface_graph @@ -1483,7 +1482,7 @@ PyObject *igraphmodule_Graph_get_eid(igraphmodule_GraphObject * self, PyObject_IsTrue(directed), PyObject_IsTrue(error))) return igraphmodule_handle_igraph_error(); - return Py_BuildValue("l", (long)result); + return igraphmodule_integer_t_to_PyObject(result); } /** \ingroup python_interface_graph @@ -1595,7 +1594,7 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, return NULL; } igraph_vector_destroy(weights); free(weights); - return PyFloat_FromDouble((double)diameter); + return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT); } else { if (igraph_diameter(&self->g, &diameter, /* from, to, vertex_path, edge_path */ @@ -1607,11 +1606,7 @@ PyObject *igraphmodule_Graph_diameter(igraphmodule_GraphObject * self, /* The diameter is integer in this case, except if igraph_diameter() * returned NaN or infinity for some reason */ - if (ceilf(diameter) == diameter && isfinite(diameter)) { - return PyLong_FromLong((long)diameter); - } else { - return PyFloat_FromDouble((double)diameter); - } + return igraphmodule_real_t_to_PyObject(diameter, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } } @@ -1698,7 +1693,7 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, } igraph_vector_destroy(weights); free(weights); if (from >= 0) { - return Py_BuildValue("lld", (long)from, (long)to, (double)len); + return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); } else { return Py_BuildValue("OOd", Py_None, Py_None, (double)len); } @@ -1714,15 +1709,15 @@ PyObject *igraphmodule_Graph_farthest_points(igraphmodule_GraphObject * self, /* if len is finite and integer (which it typically is, unless it's * infinite), then return a Python int as the third value; otherwise * return a float */ - if (ceilf(len) == len && isfinite(len)) { + if (ceil(len) == len && isfinite(len)) { if (from >= 0) { - return Py_BuildValue("lll", (long)from, (long)to, (long)len); + return Py_BuildValue("nnn", (Py_ssize_t)from, (Py_ssize_t)to, (Py_ssize_t)len); } else { - return Py_BuildValue("OOl", Py_None, Py_None, (long)len); + return Py_BuildValue("OOn", Py_None, Py_None, (Py_ssize_t)len); } } else { if (from >= 0) { - return Py_BuildValue("lld", (long)from, (long)to, (double)len); + return Py_BuildValue("nnd", (Py_ssize_t)from, (Py_ssize_t)to, (double)len); } else { return Py_BuildValue("OOd", Py_None, Py_None, (double)len); } @@ -1742,10 +1737,15 @@ PyObject *igraphmodule_Graph_girth(igraphmodule_GraphObject *self, igraph_integer_t girth; igraph_vector_int_t vids; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &sc)) - return NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &sc)) { + return NULL; + } + + if (igraph_vector_int_init(&vids, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } - igraph_vector_int_init(&vids, 0); if (igraph_girth(&self->g, &girth, &vids)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&vids); @@ -1754,11 +1754,12 @@ PyObject *igraphmodule_Graph_girth(igraphmodule_GraphObject *self, if (PyObject_IsTrue(sc)) { PyObject* o; - o=igraphmodule_vector_int_t_to_PyList(&vids); + o = igraphmodule_vector_int_t_to_PyList(&vids); igraph_vector_int_destroy(&vids); return o; } - return PyLong_FromLong((long)girth); + + return igraphmodule_integer_t_to_PyObject(girth); } /** @@ -4173,7 +4174,7 @@ PyObject *igraphmodule_Graph_bipartite_projection_size(igraphmodule_GraphObject if (types) { igraph_vector_bool_destroy(types); free(types); } - return Py_BuildValue("llll", (long)vcount1, (long)ecount1, (long)vcount2, (long)ecount2); + return Py_BuildValue("nnnn", (Py_ssize_t)vcount1, (Py_ssize_t)ecount1, (Py_ssize_t)vcount2, (Py_ssize_t)ecount2); } /** \ingroup python_interface_graph @@ -5321,10 +5322,11 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, igraph_vs_destroy(&vs); - if (!return_single) + if (!return_single) { result = igraphmodule_vector_int_t_to_PyList(&res); - else - result = PyLong_FromLong((long)VECTOR(res)[0]); + } else { + result = igraphmodule_integer_t_to_PyObject(VECTOR(res)[0]); + } igraph_vector_int_destroy(&res); @@ -6235,7 +6237,7 @@ PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", "neighbors", NULL }; PyObject *checks = Py_True, *neis = Py_None; - long int source = -1, target = -1, result; + long int source = -1, target = -1; igraph_integer_t res; igraph_vconn_nei_t neighbors = IGRAPH_VCONN_NEI_ERROR; @@ -6261,10 +6263,7 @@ PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, return NULL; } - if (!IGRAPH_FINITE(res)) return Py_BuildValue("d", (double)res); - - result = (long)res; - return Py_BuildValue("l", result); + return igraphmodule_integer_t_to_PyObject(res); } /********************************************************************** @@ -6337,14 +6336,12 @@ PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, */ PyObject *igraphmodule_Graph_dyad_census(igraphmodule_GraphObject *self) { igraph_integer_t mut, asym, nul; - PyObject *list; if (igraph_dyad_census(&self->g, &mut, &asym, &nul)) { return igraphmodule_handle_igraph_error(); } - list = Py_BuildValue("lll", (long)mut, (long)asym, (long)nul); - return list; + return Py_BuildValue("nnn", (Py_ssize_t)mut, (Py_ssize_t)asym, (Py_ssize_t)nul); } typedef struct { @@ -6479,7 +6476,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, } igraph_vector_destroy(&cut_prob); - return PyLong_FromLong((long)result); + return igraphmodule_integer_t_to_PyObject(result); } /** \ingroup python_interface_graph @@ -6543,7 +6540,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s } igraph_vector_destroy(&cut_prob); - return PyLong_FromLong((long)result); + return igraphmodule_integer_t_to_PyObject(result); } /** \ingroup python_interface_graph @@ -7950,8 +7947,8 @@ PyObject *igraphmodule_Graph_Read_DIMACS(PyTypeObject * type, CREATE_GRAPH_FROM_TYPE(self, g, type); - return Py_BuildValue("NiiN", (PyObject *) self, (long)source, - (long)target, capacity_obj); + return Py_BuildValue("NnnN", (PyObject *) self, (Py_ssize_t)source, + (Py_ssize_t)target, capacity_obj); } /** \ingroup python_interface_graph @@ -8676,7 +8673,7 @@ PyObject *igraphmodule_Graph_isoclass(igraphmodule_GraphObject * self, } } - return PyLong_FromLong((long)isoclass); + return igraphmodule_integer_t_to_PyObject(isoclass); } /** \ingroup python_interface_graph @@ -8858,8 +8855,8 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_node_compat_fn( igraph_bool_t retval; PyObject *result; - result = PyObject_CallFunction(data->node_compat_fn, "OOll", - data->graph1, data->graph2, (long)cand1, (long)cand2); + result = PyObject_CallFunction(data->node_compat_fn, "OOnn", + data->graph1, data->graph2, (Py_ssize_t)cand1, (Py_ssize_t)cand2); if (result == NULL) { /* Error in callback, return 0 */ @@ -8882,8 +8879,8 @@ igraph_bool_t igraphmodule_i_Graph_isomorphic_vf2_edge_compat_fn( igraph_bool_t retval; PyObject *result; - result = PyObject_CallFunction(data->edge_compat_fn, "OOll", - data->graph1, data->graph2, (long)cand1, (long)cand2); + result = PyObject_CallFunction(data->edge_compat_fn, "OOnn", + data->graph1, data->graph2, (Py_ssize_t)cand1, (Py_ssize_t)cand2); if (result == NULL) { /* Error in callback, return 0 */ @@ -9129,7 +9126,7 @@ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *se if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } if (edge_color2) { igraph_vector_int_destroy(edge_color2); free(edge_color2); } - return Py_BuildValue("l", (long)result); + return igraphmodule_integer_t_to_PyObject(result); } /** \ingroup python_interface_graph @@ -9466,7 +9463,7 @@ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject if (edge_color1) { igraph_vector_int_destroy(edge_color1); free(edge_color1); } if (edge_color2) { igraph_vector_int_destroy(edge_color2); free(edge_color2); } - return Py_BuildValue("l", (long)result); + return igraphmodule_integer_t_to_PyObject(result); } /** \ingroup python_interface_graph @@ -10888,7 +10885,7 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, static char *kwlist[] = { "min", "max", NULL }; PyObject *list, *item; long int min_size = 0, max_size = 0; - long int i, j, n; + igraph_integer_t i, j, n; igraph_vector_ptr_t result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ll", kwlist, @@ -10906,22 +10903,23 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); + n = igraph_vector_ptr_size(&result); list = PyList_New(n); - if (!list) + if (!list) { return NULL; + } for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { - for (j = i; j < n; j++) + for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); + } igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; - } - else { + } else { PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); @@ -10937,7 +10935,7 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject * self) { PyObject *list, *item; - long int i, j, n; + igraph_integer_t i, j, n; igraph_vector_ptr_t result; if (igraph_vector_ptr_init(&result, 0)) { @@ -10950,22 +10948,23 @@ PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject * self) return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); + n = igraph_vector_ptr_size(&result); list = PyList_New(n); - if (!list) + if (!list) { return NULL; + } for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { - for (j = i; j < n; j++) + for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); + } igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; - } - else { + } else { PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); @@ -11065,13 +11064,13 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { - for (j = i; j < n; j++) + for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); + } igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; - } - else { + } else { PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); @@ -11098,14 +11097,13 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_clique_number(igraphmodule_GraphObject * self) { - PyObject *result; igraph_integer_t i; - if (igraph_clique_number(&self->g, &i)) + if (igraph_clique_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); + } - result = PyLong_FromLong((long)i); - return result; + return igraphmodule_integer_t_to_PyObject(i); } /** \ingroup python_interface_graph @@ -11118,7 +11116,7 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject static char *kwlist[] = { "min", "max", NULL }; PyObject *list, *item; long int min_size = 0, max_size = 0; - long int i, j, n; + igraph_integer_t i, j, n; igraph_vector_ptr_t result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ll", kwlist, @@ -11136,22 +11134,23 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); + n = igraph_vector_ptr_size(&result); list = PyList_New(n); - if (!list) + if (!list) { return NULL; + } for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { - for (j = i; j < n; j++) + for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); + } igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; - } - else { + } else { PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); @@ -11169,7 +11168,7 @@ PyObject * self) { PyObject *list, *item; - long int i, j, n; + igraph_integer_t i, j, n; igraph_vector_ptr_t result; if (igraph_vector_ptr_init(&result, 0)) { @@ -11182,7 +11181,7 @@ PyObject return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); + n = igraph_vector_ptr_size(&result); list = PyList_New(n); if (!list) return NULL; @@ -11191,13 +11190,13 @@ PyObject igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { - for (j = i; j < n; j++) + for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); + } igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; - } - else { + } else { PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); @@ -11215,7 +11214,7 @@ PyObject * self) { PyObject *list, *item; - long int i, j, n; + igraph_integer_t i, j, n; igraph_vector_ptr_t result; if (igraph_vector_ptr_init(&result, 0)) { @@ -11228,22 +11227,23 @@ PyObject return igraphmodule_handle_igraph_error(); } - n = (long)igraph_vector_ptr_size(&result); + n = igraph_vector_ptr_size(&result); list = PyList_New(n); - if (!list) + if (!list) { return NULL; + } for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); if (!item) { - for (j = i; j < n; j++) + for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); + } igraph_vector_ptr_destroy_all(&result); Py_DECREF(list); return NULL; - } - else { + } else { PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); @@ -11259,14 +11259,13 @@ PyObject PyObject *igraphmodule_Graph_independence_number(igraphmodule_GraphObject * self) { - PyObject *result; igraph_integer_t i; - if (igraph_independence_number(&self->g, &i)) + if (igraph_independence_number(&self->g, &i)) { return igraphmodule_handle_igraph_error(); + } - result = PyLong_FromLong((long)i); - return result; + return igraphmodule_integer_t_to_PyObject(i); } /********************************************************************** From 197b8736b8f7f2ad86c966af354e459b96704121 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 Sep 2021 00:02:18 +0200 Subject: [PATCH 0375/1681] refactor: explicit (long int) casts also eliminated --- src/_igraph/attributes.c | 8 +++++--- src/_igraph/graphobject.c | 16 +++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 7ffa56e73..4b9406d11 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -976,7 +976,7 @@ static PyObject* igraphmodule_i_ac_first(PyObject* values, */ static PyObject* igraphmodule_i_ac_random(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *item, *num; PyObject *random_module = PyImport_ImportModule("random"); PyObject *random_func; @@ -993,7 +993,7 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; - long int n = igraph_vector_int_size(v); + igraph_integer_t n = igraph_vector_int_size(v); if (n > 0) { num = PyObject_CallObject(random_func, 0); @@ -1002,7 +1002,9 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, Py_DECREF(res); return 0; } - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[(long int)(n*PyFloat_AsDouble(num))]); + item = PyList_GET_ITEM( + values, VECTOR(*v)[(igraph_integer_t)(n * PyFloat_AsDouble(num))] + ); Py_DECREF(num); } else { item = Py_None; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index fd4c44d86..35c147c78 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -757,10 +757,11 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, return NULL; } - if (!return_single) + if (!return_single) { list = igraphmodule_vector_int_t_to_PyList(&result); - else - list = PyLong_FromLong((long int)VECTOR(result)[0]); + } else { + list = igraphmodule_integer_t_to_PyObject(VECTOR(result)[0]); + } igraph_vector_int_destroy(&result); igraph_vs_destroy(&vs); @@ -1183,10 +1184,11 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, return NULL; } - if (!return_single) + if (!return_single) { list = igraphmodule_vector_int_t_to_PyList(&result); - else - list = PyLong_FromLong((long int)VECTOR(result)[0]); + } else { + list = igraphmodule_integer_t_to_PyObject(VECTOR(result)[0]); + } igraph_vector_int_destroy(&result); igraph_es_destroy(&es); @@ -12116,7 +12118,7 @@ PyObject *igraphmodule_Graph___graph_as_capsule__(igraphmodule_GraphObject * * module without any additional conversions. */ PyObject *igraphmodule_Graph__raw_pointer(igraphmodule_GraphObject *self) { - return PyLong_FromLong((long int)&self->g); + return PyLong_FromVoidPtr(&self->g); } /** \ingroup python_interface_internal From 2794f983fba516d1db4bbd02252a79a83cd68166 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 7 Sep 2021 00:07:52 +0200 Subject: [PATCH 0376/1681] refactor: igraph_*_size functions return an igraph_integer_t so use that --- src/_igraph/attributes.c | 28 ++++++++++++++-------------- src/_igraph/vertexobject.c | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 4b9406d11..1bd7aa831 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -816,14 +816,14 @@ static int igraphmodule_i_attribute_permute_edges(const igraph_t *graph, */ static PyObject* igraphmodule_i_ac_func(PyObject* values, const igraph_vector_ptr_t *merges, PyObject* func) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *list, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; - long int j, n = igraph_vector_int_size(v); + igraph_integer_t j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { @@ -884,14 +884,14 @@ static PyObject* igraphmodule_i_ac_builtin_func(PyObject* values, */ static PyObject* igraphmodule_i_ac_sum(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; igraph_real_t num = 0.0, sum = 0.0; - long int j, n = igraph_vector_int_size(v); + igraph_integer_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -918,14 +918,14 @@ static PyObject* igraphmodule_i_ac_sum(PyObject* values, */ static PyObject* igraphmodule_i_ac_prod(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; igraph_real_t num = 1.0, prod = 1.0; - long int j, n = igraph_vector_int_size(v); + igraph_integer_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -952,13 +952,13 @@ static PyObject* igraphmodule_i_ac_prod(PyObject* values, */ static PyObject* igraphmodule_i_ac_first(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; - long int n = igraph_vector_int_size(v); + igraph_integer_t n = igraph_vector_int_size(v); item = n > 0 ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; Py_INCREF(item); @@ -1027,13 +1027,13 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, */ static PyObject* igraphmodule_i_ac_last(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; - long int n = igraph_vector_int_size(v); + igraph_integer_t n = igraph_vector_int_size(v); item = (n > 0) ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; Py_INCREF(item); @@ -1051,14 +1051,14 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, */ static PyObject* igraphmodule_i_ac_mean(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; igraph_real_t num = 0.0, mean = 0.0; - long int j, n = igraph_vector_int_size(v); + igraph_integer_t j, n = igraph_vector_int_size(v); for (j = 0; j < n; ) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); @@ -1087,13 +1087,13 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, */ static PyObject* igraphmodule_i_ac_median(PyObject* values, const igraph_vector_ptr_t *merges) { - long int i, len = igraph_vector_ptr_size(merges); + igraph_integer_t i, len = igraph_vector_ptr_size(merges); PyObject *res, *list, *item; res = PyList_New(len); for (i = 0; i < len; i++) { igraph_vector_int_t *v = (igraph_vector_int_t*)VECTOR(*merges)[i]; - long int j, n = igraph_vector_int_size(v); + igraph_integer_t j, n = igraph_vector_int_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 9a470d66c..6da7cb720 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -654,7 +654,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb #define GRAPH_PROXY_METHOD_PP(FUNC, METHODNAME, POSTPROCESS) \ PyObject* igraphmodule_Vertex_##FUNC(igraphmodule_VertexObject* self, PyObject* args, PyObject* kwds) { \ PyObject *new_args, *item, *result; \ - long int i, num_args = args ? PyTuple_Size(args)+1 : 1; \ + Py_ssize_t i, num_args = args ? PyTuple_Size(args)+1 : 1; \ \ /* Prepend ourselves to args */ \ new_args = PyTuple_New(num_args); \ From a72ee5d807a1883792f3a3cecc1f039b918d5d26 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 8 Sep 2021 20:38:04 +0200 Subject: [PATCH 0377/1681] refactor: removed a few more casts --- src/_igraph/arpackobject.c | 2 +- src/_igraph/graphobject.c | 43 +++++++++++++++++++------------------- src/_igraph/igraphmodule.c | 23 ++++++++++++-------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 7f31f506c..d10cc8d9d 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -78,7 +78,7 @@ PyObject* igraphmodule_ARPACKOptions_getattr( } else if (strcmp(attrname, "nev") == 0) { result=PyLong_FromLong(self->params.nev); } else if (strcmp(attrname, "tol") == 0) { - result=PyFloat_FromDouble((double)self->params.tol); + result=PyFloat_FromDouble(self->params.tol); } else if (strcmp(attrname, "ncv") == 0) { result=PyLong_FromLong(self->params.ncv); } else if (strcmp(attrname, "ldv") == 0) { diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 35c147c78..983230934 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -938,7 +938,7 @@ PyObject *igraphmodule_Graph_density(igraphmodule_GraphObject * self, return NULL; } - return Py_BuildValue("d", (double)result); + return igraphmodule_real_t_to_PyObject(result, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -1320,7 +1320,7 @@ PyObject *igraphmodule_Graph_reciprocity(igraphmodule_GraphObject * self, return NULL; } - return Py_BuildValue("d", (double)result); + return igraphmodule_real_t_to_PyObject(result, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -1914,7 +1914,7 @@ PyObject *igraphmodule_Graph_radius(igraphmodule_GraphObject * self, return NULL; } - return PyFloat_FromDouble((double)radius); + return igraphmodule_real_t_to_PyObject(radius, IGRAPHMODULE_TYPE_FLOAT_IF_FRACTIONAL_ELSE_INT); } /** \ingroup python_interface_graph @@ -3713,7 +3713,7 @@ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *sel return NULL; } - return Py_BuildValue("d", (double)(res)); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -3748,7 +3748,7 @@ PyObject *igraphmodule_Graph_assortativity(igraphmodule_GraphObject *self, PyObj return NULL; } - return Py_BuildValue("d", (double)(res)); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -3769,7 +3769,7 @@ PyObject *igraphmodule_Graph_assortativity_degree(igraphmodule_GraphObject *self return NULL; } - return Py_BuildValue("d", (double)(res)); + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -3814,7 +3814,7 @@ PyObject *igraphmodule_Graph_authority_score( if (res_o == NULL) return igraphmodule_handle_igraph_error(); if (PyObject_IsTrue(return_eigenvalue)) { - PyObject *ev_o = PyFloat_FromDouble((double)value); + PyObject *ev_o = igraphmodule_real_t_to_PyObject(value, IGRAPHMODULE_TYPE_FLOAT); if (ev_o == NULL) { Py_DECREF(res_o); return igraphmodule_handle_igraph_error(); @@ -4445,10 +4445,11 @@ PyObject *igraphmodule_Graph_constraint(igraphmodule_GraphObject * self, return NULL; } - if (!return_single) + if (!return_single) { list = igraphmodule_vector_t_to_PyList(&result, IGRAPHMODULE_TYPE_FLOAT); - else - list = PyFloat_FromDouble((double)VECTOR(result)[0]); + } else { + list = igraphmodule_real_t_to_PyObject(VECTOR(result)[0], IGRAPHMODULE_TYPE_FLOAT); + } igraph_vs_destroy(&vids); igraph_vector_destroy(&result); @@ -4833,7 +4834,7 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( if (res_o == NULL) return igraphmodule_handle_igraph_error(); if (PyObject_IsTrue(return_eigenvalue)) { - PyObject *ev_o = PyFloat_FromDouble((double)value); + PyObject *ev_o = igraphmodule_real_t_to_PyObject(value, IGRAPHMODULE_TYPE_FLOAT); if (ev_o == NULL) { Py_DECREF(res_o); return igraphmodule_handle_igraph_error(); @@ -5196,7 +5197,7 @@ PyObject *igraphmodule_Graph_hub_score( if (res_o == NULL) return igraphmodule_handle_igraph_error(); if (PyObject_IsTrue(return_eigenvalue)) { - PyObject *ev_o = PyFloat_FromDouble((double)value); + PyObject *ev_o = igraphmodule_real_t_to_PyObject(value, IGRAPHMODULE_TYPE_FLOAT); if (ev_o == NULL) { Py_DECREF(res_o); return igraphmodule_handle_igraph_error(); @@ -6065,7 +6066,7 @@ PyObject *igraphmodule_Graph_transitivity_undirected(igraphmodule_GraphObject { static char *kwlist[] = { "mode", NULL }; igraph_real_t res; - PyObject *r, *mode_o = Py_None; + PyObject *mode_o = Py_None; igraph_transitivity_mode_t mode = IGRAPH_TRANSITIVITY_NAN; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -6080,8 +6081,7 @@ PyObject *igraphmodule_Graph_transitivity_undirected(igraphmodule_GraphObject return NULL; } - r = Py_BuildValue("d", (double)(res)); - return r; + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -6094,7 +6094,7 @@ PyObject *igraphmodule_Graph_transitivity_avglocal_undirected(igraphmodule_Graph { static char *kwlist[] = { "mode", NULL }; igraph_real_t res; - PyObject *r, *mode_o = Py_None; + PyObject *mode_o = Py_None; igraph_transitivity_mode_t mode = IGRAPH_TRANSITIVITY_NAN; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O", kwlist, &mode_o)) @@ -6108,8 +6108,7 @@ PyObject *igraphmodule_Graph_transitivity_avglocal_undirected(igraphmodule_Graph return NULL; } - r = Py_BuildValue("d", (double)(res)); - return r; + return igraphmodule_real_t_to_PyObject(res, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -10162,7 +10161,8 @@ PyObject *igraphmodule_Graph_maxflow_value(igraphmodule_GraphObject * self, } igraph_vector_destroy(&capacity_vector); - return Py_BuildValue("d", (double)result); + + return igraphmodule_real_t_to_PyObject(result, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -10443,7 +10443,8 @@ PyObject *igraphmodule_Graph_mincut_value(igraphmodule_GraphObject * self, } igraph_vector_destroy(&capacity_vector); - return Py_BuildValue("d", (double)result); + + return igraphmodule_real_t_to_PyObject(result, IGRAPHMODULE_TYPE_FLOAT); } /** \ingroup python_interface_graph @@ -11346,7 +11347,7 @@ PyObject *igraphmodule_Graph_modularity(igraphmodule_GraphObject *self, igraph_vector_destroy(weights); free(weights); } - return Py_BuildValue("d", (double)modularity); + return igraphmodule_real_t_to_PyObject(modularity, IGRAPHMODULE_TYPE_FLOAT); } /** diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 9dbd3afa0..bdc0eda59 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -152,16 +152,16 @@ static int igraphmodule_igraph_interrupt_hook(void* data) { int igraphmodule_igraph_progress_hook(const char* message, igraph_real_t percent, void* data) { PyObject* progress_handler = GETSTATE(0)->progress_handler; + PyObject *result; if (progress_handler) { - PyObject *result; if (PyCallable_Check(progress_handler)) { - result=PyObject_CallFunction(progress_handler, - "sd", message, (double)percent); - if (result) + result = PyObject_CallFunction(progress_handler, "sd", message, (double)percent); + if (result) { Py_DECREF(result); - else + } else { return IGRAPH_INTERRUPTED; + } } } @@ -383,14 +383,18 @@ PyObject* igraphmodule_compare_communities(PyObject *self, igraph_real_t result; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", kwlist, - &comm1_o, &comm2_o, &method_o)) + &comm1_o, &comm2_o, &method_o)) { return NULL; + } - if (igraphmodule_PyObject_to_community_comparison_t(method_o, &method)) + if (igraphmodule_PyObject_to_community_comparison_t(method_o, &method)) { return NULL; + } - if (igraphmodule_PyObject_to_vector_int_t(comm1_o, &comm1)) + if (igraphmodule_PyObject_to_vector_int_t(comm1_o, &comm1)) { return NULL; + } + if (igraphmodule_PyObject_to_vector_int_t(comm2_o, &comm2)) { igraph_vector_int_destroy(&comm1); return NULL; @@ -402,10 +406,11 @@ PyObject* igraphmodule_compare_communities(PyObject *self, igraph_vector_int_destroy(&comm2); return NULL; } + igraph_vector_int_destroy(&comm1); igraph_vector_int_destroy(&comm2); - return PyFloat_FromDouble((double)result); + return igraphmodule_real_t_to_PyObject(result, IGRAPHMODULE_TYPE_FLOAT); } From e7e9ff9e9739c248f84260a607a939f3236ee9b2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 14:28:34 +0200 Subject: [PATCH 0378/1681] refactor: gotten rid of PyLong_AsInt() where the result could be larger than an int --- src/_igraph/attributes.c | 6 ++--- src/_igraph/convert.c | 51 +++++++++++++++++++++++--------------- src/_igraph/convert.h | 2 -- src/_igraph/vertexobject.c | 18 +++++++++++--- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 1bd7aa831..e74a296c4 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -136,7 +136,6 @@ int igraphmodule_PyObject_matches_attribute_record(PyObject* object, igraph_attr int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_integer_t* vid) { igraphmodule_i_attribute_struct* attrs = ATTR_STRUCT(graph); PyObject* o_vid = NULL; - int tmp; if (graph) { attrs = ATTR_STRUCT(graph); @@ -155,10 +154,9 @@ int igraphmodule_get_vertex_id_by_name(igraph_t *graph, PyObject* o, igraph_inte return 1; } - if (PyLong_AsInt(o_vid, &tmp)) + if (igraphmodule_PyObject_to_integer_t(o_vid, vid)) { return 1; - - *vid = tmp; + } return 0; } diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index dd4f9e3ef..0571cc706 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -43,7 +43,8 @@ * \brief Converts a Python long to a C int * * This is similar to PyLong_AsLong, but it checks for overflow first and - * throws an exception if necessary. + * throws an exception if necessary. This variant is needed for enum conversions + * because we assume that enums fit into an int. * * Returns -1 if there was an error, 0 otherwise. */ @@ -799,11 +800,21 @@ int PyLong_to_integer_t(PyObject* obj, igraph_integer_t* v) { /* here the assumption is that sizeof(long long) == 64 bits; anyhow, this * is the widest integer type that we can convert a PyLong to so we cannot * do any better than this */ - *v = PyLong_AsLongLong(obj); + long long int dummy = PyLong_AsLongLong(obj); + if (PyErr_Occurred()) { + return 1; + } + *v = dummy; } else { - int dummy; - PyLong_AsInt(obj, &dummy); - *v = (igraph_integer_t)dummy; + /* this is either 32-bit igraph, or some weird, officially not-yet-supported + * igraph flavour. Let's try to be on the safe side and assume 32-bit. long + * ints are at least 32 bits so we will fit, otherwise Python will raise + * an OverflowError on its own */ + long int dummy = PyLong_AsLong(obj); + if (PyErr_Occurred()) { + return 1; + } + *v = dummy; } return 0; } @@ -2706,8 +2717,6 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph) { - int retval, tmp; - if (o == Py_None || o == 0) { *vid = 0; } else if (PyLong_Check(o)) { @@ -2727,20 +2736,19 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g PyObject* num = PyNumber_Index(o); if (num) { if (PyLong_Check(num)) { - retval = PyLong_AsInt(num, &tmp); - if (retval) { + if (igraphmodule_PyObject_to_integer_t(num, vid)) { Py_DECREF(num); return 1; } - *vid = tmp; } else { PyErr_SetString(PyExc_TypeError, "PyNumber_Index returned invalid type"); Py_DECREF(num); return 1; } Py_DECREF(num); - } else + } else { return 1; + } } else { PyErr_SetString(PyExc_TypeError, "only numbers, strings or igraph.Vertex objects can be converted to vertex IDs"); return 1; @@ -2921,7 +2929,7 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *graph) { - int retval, tmp; + int retval; igraph_integer_t vid1, vid2; if (o == Py_None || o == 0) { @@ -2939,35 +2947,38 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g PyObject* num = PyNumber_Index(o); if (num) { if (PyLong_Check(num)) { - retval = PyLong_AsInt(num, &tmp); - if (retval) { + if (igraphmodule_PyObject_to_integer_t(num, eid)) { Py_DECREF(num); return 1; } - *eid = tmp; } else { PyErr_SetString(PyExc_TypeError, "PyNumber_Index returned invalid type"); Py_DECREF(num); return 1; } Py_DECREF(num); - } else + } else { return 1; + } } else if (graph != 0 && PyTuple_Check(o)) { PyObject *o1, *o2; o1 = PyTuple_GetItem(o, 0); - if (!o1) + if (!o1) { return 1; + } - o2 = PyTuple_GetItem(o, 1); + o2 = PyTuple_GetItem(o, 1); { if (!o2) return 1; + } - if (igraphmodule_PyObject_to_vid(o1, &vid1, graph)) + if (igraphmodule_PyObject_to_vid(o1, &vid1, graph)) { return 1; - if (igraphmodule_PyObject_to_vid(o2, &vid2, graph)) + } + if (igraphmodule_PyObject_to_vid(o2, &vid2, graph)) { return 1; + } retval = igraph_get_eid(graph, eid, vid1, vid2, 1, 0); if (retval == IGRAPH_EINVVID) { diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index 12139427a..0bbcdd2fa 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -44,8 +44,6 @@ typedef struct { int value; } igraphmodule_enum_translation_table_entry_t; -int PyLong_AsInt(PyObject* obj, int* result); - /* Conversion from PyObject to enum types */ int igraphmodule_PyObject_to_enum(PyObject *o, diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 6da7cb720..ba3d50186 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -601,17 +601,22 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje for (i = 0; i < n; i++) { PyObject* idx = PyList_GET_ITEM(obj, i); PyObject* v; - int idx_int; + igraph_integer_t idx_int; if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_edge_list expected list of integers"); return NULL; } - if (PyLong_AsInt(idx, &idx_int)) + if (igraphmodule_PyObject_to_integer_t(idx, &idx_int)) { return NULL; + } v = igraphmodule_Edge_New(vertex->gref, idx_int); + if (!v) { + return NULL; + } + PyList_SetItem(obj, i, v); /* reference to v stolen, reference to idx discarded */ } @@ -633,17 +638,22 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb for (i = 0; i < n; i++) { PyObject* idx = PyList_GET_ITEM(obj, i); PyObject* v; - int idx_int; + igraph_integer_t idx_int; if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_vertex_list expected list of integers"); return NULL; } - if (PyLong_AsInt(idx, &idx_int)) + if (igraphmodule_PyObject_to_integer_t(idx, &idx_int)) { return NULL; + } v = igraphmodule_Vertex_New(vertex->gref, idx_int); + if (!v) { + return NULL; + } + PyList_SetItem(obj, i, v); /* reference to v stolen, reference to idx discarded */ } From 73686f07fde7f38ba33be89e10ab3de5157dcdc4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 15:01:04 +0200 Subject: [PATCH 0379/1681] refactor: eliminating PyLong_AsLong() in a few places --- src/_igraph/arpackobject.c | 19 ++++++++++++++----- src/_igraph/graphobject.c | 33 ++++++++++++++++++++------------- src/_igraph/indexing.c | 2 +- src/_igraph/vertexseqobject.c | 9 +++++++-- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index d10cc8d9d..5fa78d6f7 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -21,6 +21,7 @@ */ #include "arpackobject.h" +#include "convert.h" #include "graphobject.h" #include "error.h" @@ -120,6 +121,8 @@ PyObject* igraphmodule_ARPACKOptions_getattr( int igraphmodule_ARPACKOptions_setattr( igraphmodule_ARPACKOptionsObject* self, char* attrname, PyObject* value) { + igraph_integer_t igraph_int; + if (value == 0) { PyErr_SetString(PyExc_TypeError, "attribute can not be deleted"); return -1; @@ -127,10 +130,13 @@ int igraphmodule_ARPACKOptions_setattr( if (strcmp(attrname, "maxiter") == 0 || strcmp(attrname, "mxiter") == 0) { if (PyLong_Check(value)) { - long int n=PyLong_AsLong(value); - if (n>0) - self->params.mxiter=(igraph_integer_t)n; - else { + if (igraphmodule_PyObject_to_integer_t(value, &igraph_int)) { + return -1; + } + + if (igraph_int > 0) { + self->params.mxiter = igraph_int; + } else { PyErr_SetString(PyExc_ValueError, "maxiter must be positive"); return -1; } @@ -140,7 +146,10 @@ int igraphmodule_ARPACKOptions_setattr( } } else if (strcmp(attrname, "tol") == 0) { if (PyLong_Check(value)) { - self->params.tol = (igraph_real_t) PyLong_AsLong(value); + if (igraphmodule_PyObject_to_integer_t(value, &igraph_int)) { + return -1; + } + self->params.tol = igraph_int; } else if (PyFloat_Check(value)) { self->params.tol = (igraph_real_t) PyFloat_AsDouble(value); } else { diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 983230934..50b7d20ca 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2025,8 +2025,9 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, m = 1; + long n; float power = 1.0f, zero_appeal = 1.0f; + igraph_integer_t m = 1; igraph_vector_int_t outseq; igraph_t *start_from = 0; igraph_barabasi_algorithm_t algo = IGRAPH_BARABASI_PSUMTREE; @@ -2044,8 +2045,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, &start_from_o)) return NULL; - if (igraphmodule_PyObject_to_barabasi_algorithm_t(implementation_o, - &algo)) + if (igraphmodule_PyObject_to_barabasi_algorithm_t(implementation_o, &algo)) return NULL; if (igraphmodule_PyObject_to_igraph_t(start_from_o, &start_from)) @@ -2062,12 +2062,13 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, } else if (m_obj != 0) { /* let's check whether we have a constant out-degree or a list */ if (PyLong_Check(m_obj)) { - m = PyLong_AsLong(m_obj); + if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { + return NULL; + } igraph_vector_int_init(&outseq, 0); } else if (PyList_Check(m_obj)) { if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { - /* something bad happened during conversion */ - return NULL; + return NULL; } } else { PyErr_SetString(PyExc_TypeError, "m must be an integer or a list of integers"); @@ -2077,7 +2078,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, if (igraph_barabasi_game(&g, (igraph_integer_t) n, (igraph_real_t) power, - (igraph_integer_t) m, + m, &outseq, PyObject_IsTrue(outpref), (igraph_real_t) zero_appeal, PyObject_IsTrue(directed), algo, @@ -3174,8 +3175,9 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, m = 0, window = 0; + long n, window = 0; float power = 0.0f, zero_appeal = 0.0f; + igraph_integer_t m = 0; igraph_vector_int_t outseq; PyObject *m_obj, *outpref = Py_False, *directed = Py_False; @@ -3195,7 +3197,9 @@ NULL }; // let's check whether we have a constant out-degree or a list if (PyLong_Check(m_obj)) { - m = PyLong_AsLong(m_obj); + if (igraphmodule_PyObject_to_integer_t(m_obj, &m)) { + return NULL; + } igraph_vector_int_init(&outseq, 0); } else if (PyList_Check(m_obj)) { @@ -3208,7 +3212,7 @@ NULL }; if (igraph_recent_degree_game(&g, (igraph_integer_t) n, (igraph_real_t) power, (igraph_integer_t) window, - (igraph_integer_t) m, &outseq, + m, &outseq, PyObject_IsTrue(outpref), (igraph_real_t) zero_appeal, PyObject_IsTrue(directed))) { @@ -6516,9 +6520,12 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s if (PyLong_Check(sample)) { /* samples chosen randomly */ - long int ns = PyLong_AsLong(sample); - if (igraph_motifs_randesu_estimate(&self->g, &result, (igraph_integer_t) size, - &cut_prob, (igraph_integer_t) ns, 0)) { + igraph_integer_t ns; + if (igraphmodule_PyObject_to_integer_t(sample, &ns)) { + igraph_vector_destroy(&cut_prob); + return NULL; + } + if (igraph_motifs_randesu_estimate(&self->g, &result, size, &cut_prob, ns, 0)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&cut_prob); return NULL; diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index 08cd04d95..3c05cb0f8 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -220,7 +220,7 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, */ static INLINE igraph_bool_t deleting_edge(PyObject* value) { return value == Py_None || value == Py_False || - (PyLong_Check(value) && PyLong_AsLong(value) == 0); + (PyLong_Check(value) && PyLong_AsLongLong(value) == 0); } /** diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 403f01a90..2299eb074 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -542,7 +542,7 @@ PyObject* igraphmodule_VertexSeq_find(igraphmodule_VertexSeqObject *self, PyObje /* Call the callable for every vertex in the current sequence and return * the first one for which it evaluates to True */ n = PySequence_Size((PyObject*)self); - for (i=0; igref->g, item, &i)) From 2df8ecabcf8a4d278dd7c6af891bc81af1dd97e7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 15:38:37 +0200 Subject: [PATCH 0380/1681] refactor: replaced long int with igraph_integer_t in attributes.c --- src/_igraph/attributes.c | 471 +++++++++++++++++++++------------------ 1 file changed, 250 insertions(+), 221 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index e74a296c4..d6e6a3440 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -287,44 +287,42 @@ PyObject* igraphmodule_create_or_get_edge_attribute_values(const igraph_t* graph /* Initialization */ static int igraphmodule_i_attribute_init(igraph_t *graph, igraph_vector_ptr_t *attr) { igraphmodule_i_attribute_struct* attrs; - long int i, n; + igraph_integer_t i, n; - attrs=(igraphmodule_i_attribute_struct*)calloc(1, sizeof(igraphmodule_i_attribute_struct)); - if (!attrs) + attrs = (igraphmodule_i_attribute_struct*)calloc(1, sizeof(igraphmodule_i_attribute_struct)); + if (!attrs) { IGRAPH_ERROR("not enough memory to allocate attribute hashes", IGRAPH_ENOMEM); + } if (igraphmodule_i_attribute_struct_init(attrs)) { PyErr_PrintEx(0); free(attrs); IGRAPH_ERROR("not enough memory to allocate attribute hashes", IGRAPH_ENOMEM); } - graph->attr=(void*)attrs; + graph->attr = (void*)attrs; /* See if we have graph attributes */ if (attr) { - PyObject *dict=attrs->attrs[0], *value; + PyObject *dict = attrs->attrs[0], *value; char *s; n = igraph_vector_ptr_size(attr); - for (i=0; itype) { case IGRAPH_ATTRIBUTE_NUMERIC: - value=PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[0]); + value = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[0]); break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, 0, &s); - if (s == 0) - value=PyUnicode_FromString(""); - else - value=PyUnicode_FromString(s); + value = PyUnicode_FromString(s ? s : ""); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - value=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[0] ? Py_True : Py_False; + value = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[0] ? Py_True : Py_False; Py_INCREF(value); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - value=0; + value = 0; break; } if (value) { @@ -336,7 +334,7 @@ static int igraphmodule_i_attribute_init(igraph_t *graph, igraph_vector_ptr_t *a IGRAPH_FAILURE); } Py_DECREF(value); - value=0; + value = 0; } } } @@ -350,7 +348,7 @@ static void igraphmodule_i_attribute_destroy(igraph_t *graph) { /* printf("Destroying attribute table\n"); */ if (graph->attr) { - attrs=(igraphmodule_i_attribute_struct*)graph->attr; + attrs = (igraphmodule_i_attribute_struct*)graph->attr; igraphmodule_i_attribute_struct_destroy(attrs); free(attrs); } @@ -362,8 +360,8 @@ static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, igraphmodule_i_attribute_struct *fromattrs, *toattrs; PyObject *key, *value, *newval, *o=NULL; igraph_bool_t copy_attrs[3] = { ga, va, ea }; - int i, j; - Py_ssize_t pos = 0; + int i; + Py_ssize_t j, pos = 0; if (from->attr) { fromattrs=ATTR_STRUCT(from); @@ -378,7 +376,7 @@ static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, } to->attr=toattrs; - for (i=0; i<3; i++) { + for (i = 0; i < 3; i++) { if (!copy_attrs[i]) continue; @@ -391,15 +389,15 @@ static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, pos = 0; while (PyDict_Next(fromattrs->attrs[i], &pos, &key, &value)) { /* value is only borrowed, so copy it */ - if (i>0) { + if (i > 0) { newval=PyList_New(PyList_GET_SIZE(value)); - for (j=0; jattrs[i], key, newval); @@ -414,77 +412,90 @@ static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, igraph_integer_t nv, igraph_vector_ptr_t *attr) { /* Extend the end of every value in the vertex hash with nv pieces of None */ PyObject *key, *value, *dict; - long int i, j, k, l; + igraph_integer_t i, j, k, l; igraph_attribute_record_t *attr_rec; igraph_bool_t *added_attrs=0; Py_ssize_t pos = 0; - if (!graph->attr) return IGRAPH_SUCCESS; - if (nv<0) return IGRAPH_SUCCESS; + if (!graph->attr) { + return IGRAPH_SUCCESS; + } + + if (nv < 0) { + return IGRAPH_SUCCESS; + } if (attr) { added_attrs = (igraph_bool_t*)calloc((size_t)igraph_vector_ptr_size(attr), sizeof(igraph_bool_t)); - if (!added_attrs) + if (!added_attrs) { IGRAPH_ERROR("can't add vertex attributes", IGRAPH_ENOMEM); + } IGRAPH_FINALLY(free, added_attrs); } - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; - if (!PyDict_Check(dict)) + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; + if (!PyDict_Check(dict)) { IGRAPH_ERROR("vertex attribute hash type mismatch", IGRAPH_EINVAL); + } while (PyDict_Next(dict, &pos, &key, &value)) { - if (!PyList_Check(value)) + if (!PyList_Check(value)) { IGRAPH_ERROR("vertex attribute hash member is not a list", IGRAPH_EINVAL); + } + /* Check if we have specific values for the given attribute */ - attr_rec=0; + attr_rec = 0; if (attr) { - j=igraph_vector_ptr_size(attr); - for (i=0; itype) { case IGRAPH_ATTRIBUTE_NUMERIC: - o=PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyUnicode_FromString(s); + o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - o=0; + o = 0; break; } if (o) { - if (PyList_Append(value, o) == -1) + if (PyList_Append(value, o) == -1) { IGRAPH_ERROR("can't extend a vertex attribute hash member", IGRAPH_FAILURE); - else Py_DECREF(o); + } else { + Py_DECREF(o); + } } } /* Invalidate the vertex name index if needed */ - if (!strcmp(attr_rec->name, "name")) + if (!strcmp(attr_rec->name, "name")) { igraphmodule_i_attribute_struct_invalidate_vertex_name_index(ATTR_STRUCT(graph)); + } } else { - for (i=0; itype) { case IGRAPH_ATTRIBUTE_NUMERIC: - o=PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyUnicode_FromString(s); + o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - o=0; + o = 0; break; } - if (o) PyList_SET_ITEM(value, i+j, o); + if (o) { + PyList_SET_ITEM(value, i + j, o); + } } /* Invalidate the vertex name index if needed */ - if (!strcmp(attr_rec->name, "name")) + if (!strcmp(attr_rec->name, "name")) { igraphmodule_i_attribute_struct_invalidate_vertex_name_index(ATTR_STRUCT(graph)); + } PyDict_SetItemString(dict, attr_rec->name, value); Py_DECREF(value); /* compensate for PyDict_SetItemString */ } + free(added_attrs); IGRAPH_FINALLY_CLEAN(1); } @@ -552,23 +569,27 @@ static int igraphmodule_i_attribute_add_vertices(igraph_t *graph, igraph_integer /* Permuting vertices */ static int igraphmodule_i_attribute_permute_vertices(const igraph_t *graph, igraph_t *newgraph, const igraph_vector_int_t *idx) { - long int n, i; + igraph_integer_t i, n; PyObject *key, *value, *dict, *newdict, *newlist, *o; - Py_ssize_t pos=0; + Py_ssize_t pos = 0; - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; - if (!PyDict_Check(dict)) return 1; + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_VERTEX]; + if (!PyDict_Check(dict)) { + return 1; + } - newdict=PyDict_New(); - if (!newdict) return 1; + newdict = PyDict_New(); + if (!newdict) { + return 1; + } - n=igraph_vector_int_size(idx); - pos=0; + n = igraph_vector_int_size(idx); + pos = 0; while (PyDict_Next(dict, &pos, &key, &value)) { - newlist=PyList_New(n); - for (i=0; iattr) return IGRAPH_SUCCESS; - if (ne<0) return IGRAPH_SUCCESS; - + if (!graph->attr) { + return IGRAPH_SUCCESS; + } + + ne = igraph_vector_int_size(edges) / 2; + if (ne < 0) { + return IGRAPH_SUCCESS; + } + if (attr) { added_attrs = (igraph_bool_t*)calloc((size_t)igraph_vector_ptr_size(attr), sizeof(igraph_bool_t)); - if (!added_attrs) + if (!added_attrs) { IGRAPH_ERROR("can't add vertex attributes", IGRAPH_ENOMEM); + } IGRAPH_FINALLY(free, added_attrs); } - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; - if (!PyDict_Check(dict)) + dict = ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; + if (!PyDict_Check(dict)) { IGRAPH_ERROR("edge attribute hash type mismatch", IGRAPH_EINVAL); + } + while (PyDict_Next(dict, &pos, &key, &value)) { - if (!PyList_Check(value)) + if (!PyList_Check(value)) { IGRAPH_ERROR("edge attribute hash member is not a list", IGRAPH_EINVAL); + } /* Check if we have specific values for the given attribute */ - attr_rec=0; + attr_rec = 0; if (attr) { - j=igraph_vector_ptr_size(attr); - for (i=0; itype) { case IGRAPH_ATTRIBUTE_NUMERIC: - o=PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyUnicode_FromString(s); + o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - o=0; + o = 0; break; } if (o) { - if (PyList_Append(value, o) == -1) + if (PyList_Append(value, o) == -1) { IGRAPH_ERROR("can't extend an edge attribute hash member", IGRAPH_FAILURE); - else Py_DECREF(o); + } else { + Py_DECREF(o); + } } } } else { - for (i=0; itype) { case IGRAPH_ATTRIBUTE_NUMERIC: - o=PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); + o = PyFloat_FromDouble((double)VECTOR(*(igraph_vector_t*)attr_rec->value)[i]); break; case IGRAPH_ATTRIBUTE_STRING: igraph_strvector_get((igraph_strvector_t*)attr_rec->value, i, &s); - o=PyUnicode_FromString(s); + o = PyUnicode_FromString(s); break; case IGRAPH_ATTRIBUTE_BOOLEAN: - o=VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; + o = VECTOR(*(igraph_vector_bool_t*)attr_rec->value)[i] ? Py_True : Py_False; Py_INCREF(o); break; default: IGRAPH_WARNING("unsupported attribute type (not string, numeric or Boolean)"); - o=0; + o = 0; break; } - if (o) PyList_SET_ITEM(value, i+j, o); + if (o) { + PyList_SET_ITEM(value, i + j, o); + } } PyDict_SetItemString(dict, attr_rec->name, value); @@ -728,65 +764,30 @@ static int igraphmodule_i_attribute_add_edges(igraph_t *graph, const igraph_vect return IGRAPH_SUCCESS; } -/* Deleting edges, currently unused */ -/* -static void igraphmodule_i_attribute_delete_edges(igraph_t *graph, const igraph_vector_t *idx) { - long int n, i, ndeleted=0; - PyObject *key, *value, *dict, *o; - Py_ssize_t pos=0; - - dict=ATTR_STRUCT_DICT(graph)[ATTRHASH_IDX_EDGE]; - if (!PyDict_Check(dict)) return; - - n=igraph_vector_size(idx); - for (i=0; i0) { + if (attrnum > 0) { - for (i=0; i Date: Thu, 9 Sep 2021 16:02:59 +0200 Subject: [PATCH 0381/1681] refactor: centralized the code that determines what is acceptable in an igraph bool/numeric/string attribute --- src/_igraph/attributes.c | 45 ++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index d6e6a3440..8da624063 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -26,6 +26,18 @@ #include "convert.h" #include "pyhelpers.h" +static inline int PyObject_allowed_in_boolean_attribute(PyObject* o) { + return o == Py_None || o == Py_False || o == Py_True; +} + +static inline int PyObject_allowed_in_numeric_attribute(PyObject* o) { + return o == Py_None || PyNumber_Check(o); +} + +static inline int PyObject_allowed_in_string_attribute(PyObject* o) { + return o == Py_None || PyBaseString_Check(o); +} + int igraphmodule_i_attribute_struct_init(igraphmodule_i_attribute_struct *attrs) { int i; for (i=0; i<3; i++) { @@ -1359,31 +1371,28 @@ static int igraphmodule_i_attribute_get_info(const igraph_t *graph, if (PyList_Check(values)) { m = PyList_Size(values); for (l = 0; l < m && is_numeric; l++) { - o = PyList_GetItem(values, l); - if (o != Py_None && !PyNumber_Check(o)) { - is_numeric=0; + if (!PyObject_allowed_in_numeric_attribute(PyList_GetItem(values, l))) { + is_numeric = 0; } } for (l = 0; l < m && is_string; l++) { - o = PyList_GetItem(values, l); - if (o != Py_None && !PyBaseString_Check(o)) { + if (!PyObject_allowed_in_string_attribute(PyList_GetItem(values, l))) { is_string = 0; } } for (l = 0; l < m && is_boolean; l++) { - o = PyList_GetItem(values, l); - if (o != Py_None && o != Py_False && o != Py_True) { + if (!PyObject_allowed_in_boolean_attribute(PyList_GetItem(values, l))) { is_boolean = 0; } } } else { - if (values != Py_None && !PyNumber_Check(values)) { + if (!PyObject_allowed_in_numeric_attribute(values)) { is_numeric = 0; } - if (values != Py_None && !PyBaseString_Check(values)) { + if (!PyObject_allowed_in_string_attribute(values)) { is_string = 0; } - if (values != Py_None && values != Py_False && values != Py_True) { + if (!PyObject_allowed_in_boolean_attribute(values)) { is_boolean = 0; } } @@ -1461,33 +1470,29 @@ int igraphmodule_i_attribute_get_type(const igraph_t *graph, /* Go on with the checks */ is_numeric = is_string = is_boolean = 1; if (attrnum > 0) { - for (i = 0; i < j && is_numeric; i++) { - PyObject *item = PyList_GET_ITEM(o, i); - if (item != Py_None && !PyNumber_Check(item)) { + if (!PyObject_allowed_in_numeric_attribute(PyList_GET_ITEM(o, i))) { is_numeric = 0; } } for (i = 0; i < j && is_string; i++) { - PyObject *item = PyList_GET_ITEM(o, i); - if (item != Py_None && !PyBaseString_Check(item)) { + if (!PyObject_allowed_in_string_attribute(PyList_GET_ITEM(o, i))) { is_string = 0; } } for (i = 0; i < j && is_boolean; i++) { - PyObject *item = PyList_GET_ITEM(o, i); - if (item != Py_None && item != Py_True && item != Py_False) { + if (!PyObject_allowed_in_boolean_attribute(PyList_GET_ITEM(o, i))) { is_boolean = 0; } } } else { - if (o != Py_None && !PyNumber_Check(o)) { + if (!PyObject_allowed_in_numeric_attribute(o)) { is_numeric = 0; } - if (o != Py_None && !PyBaseString_Check(o)) { + if (!PyObject_allowed_in_string_attribute(o)) { is_string = 0; } - if (o != Py_None && o != Py_True && o != Py_False) { + if (!PyObject_allowed_in_boolean_attribute(o)) { is_boolean = 0; } } From 46e379fa22b79a1591dc3396904fa2b4db5478c1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 16:16:08 +0200 Subject: [PATCH 0382/1681] fix: use platform-independent inline declaration --- src/_igraph/attributes.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 8da624063..d005993aa 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -24,17 +24,18 @@ #include "attributes.h" #include "common.h" #include "convert.h" +#include "platform.h" #include "pyhelpers.h" -static inline int PyObject_allowed_in_boolean_attribute(PyObject* o) { +static INLINE int PyObject_allowed_in_boolean_attribute(PyObject* o) { return o == Py_None || o == Py_False || o == Py_True; } -static inline int PyObject_allowed_in_numeric_attribute(PyObject* o) { +static INLINE int PyObject_allowed_in_numeric_attribute(PyObject* o) { return o == Py_None || PyNumber_Check(o); } -static inline int PyObject_allowed_in_string_attribute(PyObject* o) { +static INLINE int PyObject_allowed_in_string_attribute(PyObject* o) { return o == Py_None || PyBaseString_Check(o); } From cbb0f9a249f766134ed471c39f585c857e5b97ea Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 16:16:32 +0200 Subject: [PATCH 0383/1681] refactor: getting rid of (int) casts --- src/_igraph/convert.c | 49 ++++++++++++++++++++++++++++---------- src/_igraph/edgeobject.c | 32 ++++++++++++------------- src/_igraph/graphobject.c | 4 ++-- src/_igraph/vertexobject.c | 32 ++++++++++++------------- 4 files changed, 70 insertions(+), 47 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 0571cc706..38b994356 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -58,7 +58,7 @@ int PyLong_AsInt(PyObject* obj, int* result) { PyErr_SetString(PyExc_OverflowError, "long integer too large for conversion to C int"); return -1; } - *result = (int)dummy; + *result = dummy; return 0; } @@ -379,17 +379,25 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, igraphmodule_PyObject_to_enum(value, eigen_which_position_tt, (int*) &w->pos); } else if (!strcasecmp(kv, "howmany")) { - w->howmany = (int) PyLong_AsLong(value); + if (PyLong_AsInt(value, &w->howmany)) { + return -1; + } } else if (!strcasecmp(kv, "il")) { - w->il = (int) PyLong_AsLong(value); + if (PyLong_AsInt(value, &w->il)) { + return -1; + } } else if (!strcasecmp(kv, "iu")) { - w->iu = (int) PyLong_AsLong(value); + if (PyLong_AsInt(value, &w->iu)) { + return -1; + } } else if (!strcasecmp(kv, "vl")) { w->vl = PyFloat_AsDouble(value); } else if (!strcasecmp(kv, "vu")) { w->vu = PyFloat_AsDouble(value); } else if (!strcasecmp(kv, "vestimate")) { - w->vestimate = (int) PyLong_AsLong(value); + if (PyLong_AsInt(value, &w->vestimate)) { + return -1; + } } else if (!strcasecmp(kv, "balance")) { igraphmodule_PyObject_to_enum(value, lapack_dgeevc_balance_tt, (int*) &w->balance); @@ -1843,43 +1851,58 @@ int igraphmodule_attrib_to_vector_int_t(PyObject *o, igraphmodule_GraphObject *s if (PyUnicode_Check(o)) { igraph_vector_t* dummy = 0; - long int i, n; + igraph_integer_t i, n; - if (igraphmodule_attrib_to_vector_t(o, self, &dummy, attr_type)) + if (igraphmodule_attrib_to_vector_t(o, self, &dummy, attr_type)) { return 1; + } - if (dummy == 0) + if (dummy == 0) { return 0; + } n = igraph_vector_size(dummy); result = (igraph_vector_int_t*)calloc(1, sizeof(igraph_vector_int_t)); - igraph_vector_int_init(result, n); - if (result==0) { + if (result == 0) { igraph_vector_destroy(dummy); free(dummy); PyErr_NoMemory(); return 1; } - for (i=0; ig), i; - result=PyList_New(n); - for (i=0; ig); + result = PyList_New(n); + for (i = 0; i < n; i++) { if (i != self->idx) { - Py_INCREF(Py_None); - if (PyList_SetItem(result, i, Py_None) == -1) { - Py_DECREF(Py_None); - Py_DECREF(result); - return -1; - } + Py_INCREF(Py_None); + if (PyList_SetItem(result, i, Py_None) == -1) { + Py_DECREF(Py_None); + Py_DECREF(result); + return -1; + } } else { - /* Same game with the reference count here */ - Py_INCREF(v); - if (PyList_SetItem(result, i, v) == -1) { - Py_DECREF(v); - Py_DECREF(result); - return -1; - } + /* Same game with the reference count here */ + Py_INCREF(v); + if (PyList_SetItem(result, i, v) == -1) { + Py_DECREF(v); + Py_DECREF(result); + return -1; + } } } if (PyDict_SetItem(((PyObject**)o->g.attr)[2], k, result) == -1) { diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 50b7d20ca..e507d2e78 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -9619,7 +9619,7 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, } if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, &result, - map, 0, PyObject_IsTrue(induced), (int)time_limit)) { + map, 0, PyObject_IsTrue(induced), (igraph_integer_t) time_limit)) { if (p_domains) igraph_vector_ptr_destroy_all(p_domains); igraphmodule_handle_igraph_error(); @@ -9681,7 +9681,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( } if (igraph_subisomorphic_lad(&other->g, &self->g, p_domains, 0, 0, &mappings, - PyObject_IsTrue(induced), (int)time_limit)) { + PyObject_IsTrue(induced), (igraph_integer_t) time_limit)) { igraphmodule_handle_igraph_error(); igraph_vector_ptr_destroy_all(&mappings); if (p_domains) diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index ba3d50186..041761634 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -514,24 +514,24 @@ int igraphmodule_Vertex_set_attribute(igraphmodule_VertexObject* self, PyObject* /* result is NULL, check whether there was an error */ if (!PyErr_Occurred()) { /* no, there wasn't, so we must simply add the attribute */ - int n=(int)igraph_vcount(&o->g), i; - result=PyList_New(n); - for (i=0; ig); + result = PyList_New(n); + for (i = 0; i < n; i++) { if (i != self->idx) { - Py_INCREF(Py_None); - if (PyList_SetItem(result, i, Py_None) == -1) { - Py_DECREF(Py_None); - Py_DECREF(result); - return -1; - } + Py_INCREF(Py_None); + if (PyList_SetItem(result, i, Py_None) == -1) { + Py_DECREF(Py_None); + Py_DECREF(result); + return -1; + } } else { - /* Same game with the reference count here */ - Py_INCREF(v); - if (PyList_SetItem(result, i, v) == -1) { - Py_DECREF(v); - Py_DECREF(result); - return -1; - } + /* Same game with the reference count here */ + Py_INCREF(v); + if (PyList_SetItem(result, i, v) == -1) { + Py_DECREF(v); + Py_DECREF(result); + return -1; + } } } if (PyDict_SetItem(((PyObject**)o->g.attr)[1], k, result) == -1) { From 2d2f97a63e00b3682251030e1225be124ca40a15 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 16:16:48 +0200 Subject: [PATCH 0384/1681] fix: make sure that the user cannot link a 64-bit igraph to a 32-bit Python instance --- src/_igraph/igraphmodule.c | 4 ++++ src/_igraph/pyhelpers.h | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index bdc0eda59..7e3c2b0c2 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -33,6 +33,7 @@ #include "edgeseqobject.h" #include "error.h" #include "graphobject.h" +#include "pyhelpers.h" #include "random.h" #include "vertexobject.h" #include "vertexseqobject.h" @@ -808,6 +809,9 @@ PyObject* PyInit__igraph(void) static void *PyIGraph_API[PyIGraph_API_pointers]; PyObject *c_api_object; + /* Prevent linking 64-bit igraph to 32-bit Python */ + PY_IGRAPH_ASSERT_AT_BUILD_TIME(sizeof(igraph_integer_t) >= sizeof(Py_ssize_t)); + /* Check if the module is already initialized (possibly in another Python * interpreter. If so, bail out as we don't support this. */ if (igraphmodule_initialized) { diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h index 00bff1d86..2bb848834 100644 --- a/src/_igraph/pyhelpers.h +++ b/src/_igraph/pyhelpers.h @@ -40,6 +40,8 @@ long igraphmodule_Py_HashPointer(void *p); (PyUnicode_CompareWithASCIIString(uni, string) == 0) char* PyUnicode_CopyAsString(PyObject* string); +#define PY_IGRAPH_ASSERT_AT_BUILD_TIME(condition) \ + ((void)sizeof(char[1 - 2*!(condition)])) #define PY_IGRAPH_DEPRECATED(msg) \ PyErr_WarnEx(PyExc_DeprecationWarning, (msg), 1) #define PY_IGRAPH_WARN(msg) \ From 935ff19461ccbb7d10665095c0c9d41007390711 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 21:29:39 +0200 Subject: [PATCH 0385/1681] refactor: long int to Py_ssize_t replacements --- src/_igraph/attributes.c | 3 +- src/_igraph/convert.c | 18 ++-- src/_igraph/graphobject.c | 180 +++++++++++++++++----------------- src/_igraph/igraphmodule.c | 10 +- src/_igraph/indexing.c | 11 +-- src/_igraph/pyhelpers.h | 22 +++++ src/_igraph/vertexseqobject.c | 16 +-- tests/test_generators.py | 2 +- 8 files changed, 146 insertions(+), 116 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index d005993aa..ac6b0f717 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -1341,13 +1341,12 @@ static int igraphmodule_i_attribute_get_info(const igraph_t *graph, int i, retval; Py_ssize_t j, k, l, m; - for (i=0; i<3; i++) { + for (i = 0; i < 3; i++) { igraph_strvector_t *n = names[i]; igraph_vector_int_t *t = types[i]; PyObject *dict = ATTR_STRUCT_DICT(graph)[i]; PyObject *keys; PyObject *values; - PyObject *o = 0; keys = PyDict_Keys(dict); if (!keys) { diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 38b994356..176f8a23f 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2373,7 +2373,7 @@ int igraphmodule_PyList_to_matrix_int_t(PyObject* o, igraph_matrix_int_t *m) { int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, igraph_matrix_int_t *m, int min_cols) { Py_ssize_t nr, nc, n, i, j; PyObject *row, *item; - int was_warned = 0; + int ok, was_warned = 0; /* calculate the matrix dimensions */ if (!PySequence_Check(o) || PyUnicode_Check(o)) { @@ -2403,16 +2403,22 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, i n = PySequence_Size(row); for (j = 0; j < n; j++) { item = PySequence_GetItem(row, j); + ok = 1; if (PyLong_Check(item)) { - MATRIX(*m, i, j) = (igraph_integer_t)PyLong_AsLong(item); - } else if (PyLong_Check(item)) { - MATRIX(*m, i, j) = (igraph_integer_t)PyLong_AsLong(item); + if (igraphmodule_PyObject_to_integer_t(item, &MATRIX(*m, i, j))) { + ok = 0; + } } else if (PyFloat_Check(item)) { MATRIX(*m, i, j) = (igraph_integer_t)PyFloat_AsDouble(item); - } else if (!was_warned) { + } else { + ok = 0; + } + + if (!ok && !was_warned) { PyErr_Warn(PyExc_Warning, "non-numeric value in matrix ignored"); - was_warned=1; + was_warned = 1; } + Py_DECREF(item); } Py_DECREF(row); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index e507d2e78..ac8f89b5c 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -201,13 +201,13 @@ void igraphmodule_Graph_dealloc(igraphmodule_GraphObject * self) int igraphmodule_Graph_init(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "n", "edges", "directed", "__ptr", NULL }; - long int n = 0; PyObject *edges = NULL, *dir = Py_False, *ptr_o = 0; void* ptr = 0; + Py_ssize_t n = 0; igraph_vector_int_t edges_vector; igraph_bool_t edges_vector_owned = 0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOOO!", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOOO!", kwlist, &n, &edges, &dir, &PyCapsule_Type, &ptr_o)) return -1; @@ -222,6 +222,15 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, return -1; } + if (n < 0) { + PyErr_SetString(PyExc_OverflowError, "vertex count must be non-negative"); + return -1; + } + if (n > IGRAPH_INTEGER_MAX) { + PyErr_SetString(PyExc_OverflowError, "vertex count too large"); + return -1; + } + if (ptr_o) { /* We must take ownership of an igraph graph */ ptr = PyCapsule_GetPointer(ptr_o, "__igraph_t"); @@ -238,8 +247,7 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, return -1; } - if (igraph_create - (&self->g, &edges_vector, (igraph_integer_t) n, PyObject_IsTrue(dir))) { + if (igraph_create(&self->g, &edges_vector, n, PyObject_IsTrue(dir))) { igraphmodule_handle_igraph_error(); if (edges_vector_owned) { igraph_vector_int_destroy(&edges_vector); @@ -253,7 +261,7 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, } else { /* No edge list was specified, and no previously initialized graph object * was fed into our object, so let's use igraph_empty */ - if (igraph_empty(&self->g, (igraph_integer_t) n, PyObject_IsTrue(dir))) { + if (igraph_empty(&self->g, n, PyObject_IsTrue(dir))) { igraphmodule_handle_igraph_error(); return -1; } @@ -575,12 +583,15 @@ PyObject *igraphmodule_Graph_is_tree(igraphmodule_GraphObject * self, */ PyObject *igraphmodule_Graph_add_vertices(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - long n; + Py_ssize_t n; - if (!PyArg_ParseTuple(args, "l", &n)) + if (!PyArg_ParseTuple(args, "n", &n)) { return NULL; + } + + CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraph_add_vertices(&self->g, (igraph_integer_t) n, 0)) { + if (igraph_add_vertices(&self->g, n, 0)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -605,15 +616,16 @@ PyObject *igraphmodule_Graph_delete_vertices(igraphmodule_GraphObject * self, /* no arguments means delete all. */ - /*Py_None also means all for now, but it is deprecated */ + /* Py_None used to mean 'all', but not any more */ if (list == Py_None) { - PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is " - "deprecated since igraph 0.8.3, please use " - "Graph.delete_vertices() instead"); + PyErr_SetString(PyExc_ValueError, "expected number of vertices to delete, got None"); + return NULL; } - /* this already converts no arguments and Py_None to all vertices */ - if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, 0, 0)) return NULL; + /* this already converts no arguments to all vertices */ + if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, 0, 0)) { + return NULL; + } if (igraph_delete_vertices(&self->g, vs)) { igraphmodule_handle_igraph_error(); @@ -1994,14 +2006,15 @@ PyObject *igraphmodule_Graph_Adjacency(PyTypeObject * type, */ PyObject *igraphmodule_Graph_Atlas(PyTypeObject * type, PyObject * args) { - long n; + Py_ssize_t n; igraphmodule_GraphObject *self; igraph_t g; - if (!PyArg_ParseTuple(args, "l", &n)) + if (!PyArg_ParseTuple(args, "n", &n)) { return NULL; + } - if (igraph_atlas(&g, (igraph_integer_t) n)) { + if (igraph_atlas(&g, n)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2025,7 +2038,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n; + Py_ssize_t n; float power = 1.0f, zero_appeal = 1.0f; igraph_integer_t m = 1; igraph_vector_int_t outseq; @@ -2039,7 +2052,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, { "n", "m", "outpref", "directed", "power", "zero_appeal", "implementation", "start_from", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|OOOffOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOffOO", kwlist, &n, &m_obj, &outpref, &directed, &power, &zero_appeal, &implementation_o, &start_from_o)) @@ -2051,10 +2064,7 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, if (igraphmodule_PyObject_to_igraph_t(start_from_o, &start_from)) return NULL; - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); - return NULL; - } + CHECK_SSIZE_T_RANGE(n, "vertex count"); if (m_obj == 0) { igraph_vector_int_init(&outseq, 0); @@ -2076,11 +2086,9 @@ PyObject *igraphmodule_Graph_Barabasi(PyTypeObject * type, } } - if (igraph_barabasi_game(&g, (igraph_integer_t) n, - (igraph_real_t) power, - m, + if (igraph_barabasi_game(&g, n, power, m, &outseq, PyObject_IsTrue(outpref), - (igraph_real_t) zero_appeal, + zero_appeal, PyObject_IsTrue(directed), algo, start_from)) { igraphmodule_handle_igraph_error(); @@ -2149,15 +2157,18 @@ PyObject *igraphmodule_Graph_Bipartite(PyTypeObject * type, */ PyObject *igraphmodule_Graph_De_Bruijn(PyTypeObject *type, PyObject *args, PyObject *kwds) { - long int m, n; + Py_ssize_t m, n; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = {"m", "n", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll", kwlist, &m, &n)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn", kwlist, &m, &n)) return NULL; - if (igraph_de_bruijn(&g, (igraph_integer_t) m, (igraph_integer_t) n)) { + CHECK_SSIZE_T_RANGE(m, "alphabet size (m)"); + CHECK_SSIZE_T_RANGE(n, "label length (n)"); + + if (igraph_de_bruijn(&g, m, n)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2231,19 +2242,22 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, m = -1; + Py_ssize_t n, m = -1; double p = -1.0; igraph_erdos_renyi_t t; PyObject *loops = Py_False, *directed = Py_False; static char *kwlist[] = { "n", "p", "m", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|dlOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|dnOO", kwlist, &n, &p, &m, &directed, &loops)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE((m < 0 ? 0 : m), "edge count"); + if (m == -1 && p == -1.0) { /* no density parameters were given, throw exception */ PyErr_SetString(PyExc_TypeError, "Either m or p must be given."); @@ -2257,8 +2271,7 @@ PyObject *igraphmodule_Graph_Erdos_Renyi(PyTypeObject * type, t = (m == -1) ? IGRAPH_ERDOS_RENYI_GNP : IGRAPH_ERDOS_RENYI_GNM; - if (igraph_erdos_renyi_game(&g, t, (igraph_integer_t) n, - (igraph_real_t) (m == -1 ? p : m), + if (igraph_erdos_renyi_game(&g, t, n, (m == -1 ? p : m), PyObject_IsTrue(directed), PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); @@ -2280,7 +2293,7 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, types, k; + Py_ssize_t n, types, k; PyObject *type_dist, *pref_matrix; PyObject *directed = Py_False; igraph_matrix_t pm; @@ -2288,7 +2301,7 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, char *kwlist[] = { "n", "k", "type_dist", "pref_matrix", "directed", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "llO!O!|O", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnO!O!|O", kwlist, &n, &k, &PyList_Type, &type_dist, &PyList_Type, &pref_matrix, &directed)) return NULL; @@ -2298,6 +2311,10 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, "Number of vertices and the amount of connection trials per step must be positive."); return NULL; } + + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(k, "connection trials per set"); + types = PyList_Size(type_dist); if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm)) { @@ -2319,10 +2336,7 @@ PyObject *igraphmodule_Graph_Establishment(PyTypeObject * type, return NULL; } - if (igraph_establishment_game(&g, (igraph_integer_t) n, - (igraph_integer_t) types, - (igraph_integer_t) k, &td, &pm, - PyObject_IsTrue(directed), 0)) { + if (igraph_establishment_game(&g, n, types, k, &td, &pm, PyObject_IsTrue(directed), 0)) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); @@ -2374,20 +2388,20 @@ PyObject *igraphmodule_Graph_Forest_Fire(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, ambs=1; + Py_ssize_t n, ambs = 1; double fw_prob, bw_factor=0.0; PyObject *directed = Py_False; static char *kwlist[] = {"n", "fw_prob", "bw_factor", "ambs", "directed", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ld|dlO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nd|dnO", kwlist, &n, &fw_prob, &bw_factor, &ambs, &directed)) return NULL; - if (igraph_forest_fire_game(&g, (igraph_integer_t)n, - (igraph_real_t)fw_prob, (igraph_real_t)bw_factor, - (igraph_integer_t)ambs, - (igraph_bool_t)(PyObject_IsTrue(directed)))) { + CHECK_SSIZE_T_RANGE(n, "number of nodes"); + CHECK_SSIZE_T_RANGE(n, "number of ambassadors"); + + if (igraph_forest_fire_game(&g, n, fw_prob, bw_factor, ambs, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2408,22 +2422,17 @@ PyObject *igraphmodule_Graph_Full(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n; + Py_ssize_t n; PyObject *loops = Py_False, *directed = Py_False; char *kwlist[] = { "n", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|OO", kwlist, &n, - &directed, &loops)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OO", kwlist, &n, &directed, &loops)) return NULL; - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); - return NULL; - } + CHECK_SSIZE_T_RANGE(n, "number of nodes"); - if (igraph_full(&g, (igraph_integer_t) n, PyObject_IsTrue(directed), - PyObject_IsTrue(loops))) { + if (igraph_full(&g, n, PyObject_IsTrue(directed), PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2444,31 +2453,28 @@ PyObject *igraphmodule_Graph_Full_Bipartite(PyTypeObject * type, igraph_t g; igraph_vector_bool_t vertex_types; igraph_neimode_t mode = IGRAPH_ALL; - long int n1, n2; + Py_ssize_t n1, n2; PyObject *mode_o = Py_None, *directed = Py_False, *vertex_types_o = 0; static char *kwlist[] = { "n1", "n2", "directed", "mode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|OO", kwlist, &n1, &n2, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|OO", kwlist, &n1, &n2, &directed, &mode_o)) return NULL; - if (n1 < 0 || n2 < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); + CHECK_SSIZE_T_RANGE(n1, "number of vertices in first partition"); + CHECK_SSIZE_T_RANGE(n2, "number of vertices in second partition"); + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { return NULL; } - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) - return NULL; - if (igraph_vector_bool_init(&vertex_types, n1+n2)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_full_bipartite(&g, &vertex_types, - (igraph_integer_t) n1, (igraph_integer_t) n2, - PyObject_IsTrue(directed), mode)) { + if (igraph_full_bipartite(&g, &vertex_types, n1, n2, PyObject_IsTrue(directed), mode)) { igraph_vector_bool_destroy(&vertex_types); igraphmodule_handle_igraph_error(); return NULL; @@ -2492,16 +2498,17 @@ PyObject *igraphmodule_Graph_Full_Citation(PyTypeObject *type, { igraphmodule_GraphObject *self; igraph_t g; - long n; + Py_ssize_t n; PyObject *directed = Py_False; char *kwlist[] = { "n", "directed", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|O", kwlist, &n, &directed)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|O", kwlist, &n, &directed)) return NULL; - if (igraph_full_citation(&g, (igraph_integer_t) n, - (igraph_bool_t) PyObject_IsTrue(directed))) { + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + if (igraph_full_citation(&g, n, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2521,7 +2528,7 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n; + Py_ssize_t n; double r; PyObject *torus = Py_False; PyObject *o_xs, *o_ys; @@ -2529,7 +2536,7 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, static char *kwlist[] = { "n", "radius", "torus", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ld|O", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nd|O", kwlist, &n, &r, &torus)) return NULL; @@ -2542,8 +2549,9 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, return NULL; } - if (igraph_grg_game(&g, (igraph_integer_t) n, (igraph_real_t) r, - PyObject_IsTrue(torus), &xs, &ys)) { + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + if (igraph_grg_game(&g, n, r, PyObject_IsTrue(torus), &xs, &ys)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&xs); igraph_vector_destroy(&ys); @@ -2557,6 +2565,7 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, igraph_vector_destroy(&ys); return NULL; } + o_ys = igraphmodule_vector_t_to_PyList(&ys, IGRAPHMODULE_TYPE_FLOAT); igraph_vector_destroy(&ys); if (!o_ys) { @@ -2566,6 +2575,7 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, } CREATE_GRAPH_FROM_TYPE(self, g, type); + return Py_BuildValue("NNN", (PyObject*)self, o_xs, o_ys); } @@ -2577,33 +2587,21 @@ PyObject *igraphmodule_Graph_GRG(PyTypeObject * type, PyObject *igraphmodule_Graph_Growing_Random(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long n, m; - PyObject *directed = NULL, *citation = NULL; + Py_ssize_t n, m; + PyObject *directed = Py_False, *citation = Py_False; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "m", "directed", "citation", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|O!O!", kwlist, &n, &m, - &PyBool_Type, &directed, - &PyBool_Type, &citation)) - return NULL; - - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|OO", kwlist, &n, &m, &directed, &citation)) { return NULL; } - if (m < 0) { - PyErr_SetString(PyExc_ValueError, - "Number of new edges per iteration must be positive."); - return NULL; - } + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE_POSITIVE(m, "number of new edges per iteration"); - if (igraph_growing_random_game(&g, (igraph_integer_t) n, - (igraph_integer_t) m, - (directed == Py_True), - (citation == Py_True))) { + if (igraph_growing_random_game(&g, n, m, PyObject_IsTrue(directed), PyObject_IsTrue(citation))) { igraphmodule_handle_igraph_error(); return NULL; } diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 7e3c2b0c2..8c08de894 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -332,13 +332,16 @@ PyObject* igraphmodule_community_to_membership(PyObject *self, PyObject *merges_o, *return_csize = Py_False, *result_o; igraph_matrix_int_t merges; igraph_vector_int_t result, csize, *csize_p = 0; - long int nodes, steps; + Py_ssize_t nodes, steps; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!ll|O", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!nn|O", kwlist, &PyList_Type, &merges_o, &nodes, &steps, &return_csize)) return NULL; if (igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(merges_o, &merges, 2)) return NULL; + CHECK_SSIZE_T_RANGE(nodes, "number of nodes"); + CHECK_SSIZE_T_RANGE(steps, "number of steps"); + if (igraph_vector_int_init(&result, nodes)) { igraphmodule_handle_igraph_error(); igraph_matrix_int_destroy(&merges); @@ -350,8 +353,7 @@ PyObject* igraphmodule_community_to_membership(PyObject *self, csize_p = &csize; } - if (igraph_community_to_membership(&merges, (igraph_integer_t)nodes, - (igraph_integer_t)steps, &result, csize_p)) { + if (igraph_community_to_membership(&merges, nodes, steps, &result, csize_p)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&result); if (csize_p) igraph_vector_int_destroy(csize_p); diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index 3c05cb0f8..70b76ef94 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -134,11 +134,9 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, igraph_integer_t from, igraph_vs_t* to, igraph_neimode_t neimode, PyObject* values) { igraph_vector_int_t eids; - igraph_integer_t eid; + igraph_integer_t eid, i, n, v; igraph_vit_t vit; PyObject *result = 0, *item; - long int i, n; - igraph_integer_t v; if (igraph_vs_is_all(to)) { /* Simple case: all edges */ @@ -154,12 +152,13 @@ static PyObject* igraphmodule_i_Graph_adjmatrix_get_index_row(igraph_t* graph, } for (i = 0; i < n; i++) { - eid = (igraph_integer_t)VECTOR(eids)[i]; + eid = VECTOR(eids)[i]; v = IGRAPH_OTHER(graph, eid, from); - if (values) + if (values) { item = PyList_GetItem(values, eid); - else + } else { item = PyLong_FromLong(1); + } Py_INCREF(item); PyList_SetItem(result, v, item); /* reference stolen here */ } diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h index 2bb848834..01cc689eb 100644 --- a/src/_igraph/pyhelpers.h +++ b/src/_igraph/pyhelpers.h @@ -47,4 +47,26 @@ char* PyUnicode_CopyAsString(PyObject* string); #define PY_IGRAPH_WARN(msg) \ PyErr_WarnEx(PyExc_RuntimeWarning, (msg), 1) +#define CHECK_SSIZE_T_RANGE(value, message) { \ + if ((value) < 0) { \ + PyErr_SetString(PyExc_ValueError, message " must be non-negative"); \ + return NULL; \ + } \ + if ((value) > IGRAPH_INTEGER_MAX) { \ + PyErr_SetString(PyExc_OverflowError, message " too large"); \ + return NULL; \ + } \ +} + +#define CHECK_SSIZE_T_RANGE_POSITIVE(value, message) { \ + if ((value) <= 0) { \ + PyErr_SetString(PyExc_ValueError, message " must be positive"); \ + return NULL; \ + } \ + if ((value) > IGRAPH_INTEGER_MAX) { \ + PyErr_SetString(PyExc_OverflowError, message " too large"); \ + return NULL; \ + } \ +} + #endif diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 2299eb074..717b080c7 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -127,7 +127,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, return -1; } - igraph_vs_1(&vs, (igraph_integer_t)idx); + igraph_vs_1(&vs, idx); } else { igraph_vector_int_t v; igraph_integer_t n = igraph_vcount(&((igraphmodule_GraphObject*)g)->g); @@ -199,15 +199,19 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, igraph_t *g; igraph_integer_t idx = -1; - if (!self->gref) return NULL; - g=&GET_GRAPH(self); + if (!self->gref) { + return NULL; + } + + g = &GET_GRAPH(self); + switch (igraph_vs_type(&self->vs)) { case IGRAPH_VS_ALL: if (i < 0) { i = igraph_vcount(g) + i; } if (i >= 0 && i < igraph_vcount(g)) { - idx = (igraph_integer_t)i; + idx = i; } break; case IGRAPH_VS_VECTOR: @@ -216,7 +220,7 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, i = igraph_vector_int_size(self->vs.data.vecptr) + i; } if (i >= 0 && i < igraph_vector_int_size(self->vs.data.vecptr)) { - idx = (igraph_integer_t)VECTOR(*self->vs.data.vecptr)[i]; + idx = VECTOR(*self->vs.data.vecptr)[i]; } break; case IGRAPH_VS_1: @@ -231,7 +235,7 @@ PyObject* igraphmodule_VertexSeq_sq_item(igraphmodule_VertexSeqObject* self, i = self->vs.data.seq.to - self->vs.data.seq.from + i; } if (i >= 0 && i < self->vs.data.seq.to - self->vs.data.seq.from) { - idx = self->vs.data.seq.from + (igraph_integer_t)i; + idx = self->vs.data.seq.from + i; } break; /* TODO: IGRAPH_VS_ADJ, IGRAPH_VS_NONADJ - someday :) They are unused diff --git a/tests/test_generators.py b/tests/test_generators.py index 576ca5b98..ac55a3b78 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -127,7 +127,7 @@ def testFullCitation(self): self.assertTrue(g.is_directed()) self.assertTrue(el == [(x, y) for x in range(1, 20) for y in range(x)]) - self.assertRaises(InternalError, Graph.Full_Citation, -2) + self.assertRaises(ValueError, Graph.Full_Citation, -2) def testLCF(self): g1 = Graph.LCF(12, (5, -5), 6) From d0e70d421f08a5280d12e2081196a7e09a2a62e4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 22:05:52 +0200 Subject: [PATCH 0386/1681] refactor: getting rid of more (igraph_integer_t) conversions --- src/_igraph/arpackobject.c | 2 +- src/_igraph/convert.c | 22 ++-- src/_igraph/graphobject.c | 245 +++++++++++++++++++------------------ src/_igraph/igraphmodule.c | 4 +- 4 files changed, 138 insertions(+), 135 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 5fa78d6f7..738362b50 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -151,7 +151,7 @@ int igraphmodule_ARPACKOptions_setattr( } self->params.tol = igraph_int; } else if (PyFloat_Check(value)) { - self->params.tol = (igraph_real_t) PyFloat_AsDouble(value); + self->params.tol = PyFloat_AsDouble(value); } else { PyErr_SetString(PyExc_ValueError, "integer or float expected"); return -1; diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 176f8a23f..5b75dac96 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -883,20 +883,18 @@ int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v) { if (object == NULL) { } else if (PyLong_Check(object)) { - double d = PyLong_AsDouble(object); - *v=(igraph_real_t)d; + *v = PyLong_AsDouble(object); return 0; } else if (PyFloat_Check(object)) { - double d = PyFloat_AS_DOUBLE((PyFloatObject*)object); - *v=(igraph_real_t)d; + *v = PyFloat_AS_DOUBLE((PyFloatObject*)object); return 0; } else if (PyNumber_Check(object)) { PyObject *i = PyNumber_Float(object); - double d; - if (i == NULL) return 1; - d = PyFloat_AS_DOUBLE((PyFloatObject*)i); + if (i == NULL) { + return 1; + } + *v = PyFloat_AS_DOUBLE((PyFloatObject*)i); Py_DECREF(i); - *v = (igraph_real_t)d; return 0; } PyErr_BadArgument(); @@ -2331,11 +2329,11 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count(PyObject *o, igrap for (j = 0; j < n; j++) { item = PySequence_GetItem(row, j); if (PyLong_Check(item)) { - MATRIX(*m, i, j) = (igraph_real_t)PyLong_AsLong(item); + MATRIX(*m, i, j) = PyLong_AsLong(item); } else if (PyLong_Check(item)) { - MATRIX(*m, i, j) = (igraph_real_t)PyLong_AsLong(item); + MATRIX(*m, i, j) = PyLong_AsLong(item); } else if (PyFloat_Check(item)) { - MATRIX(*m, i, j) = (igraph_real_t)PyFloat_AsDouble(item); + MATRIX(*m, i, j) = PyFloat_AsDouble(item); } else if (!was_warned) { PyErr_Warn(PyExc_Warning, "non-numeric value in matrix ignored"); was_warned=1; @@ -2418,7 +2416,7 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, i PyErr_Warn(PyExc_Warning, "non-numeric value in matrix ignored"); was_warned = 1; } - + Py_DECREF(item); } Py_DECREF(row); diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index ac8f89b5c..4ef1dce6e 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2398,7 +2398,7 @@ PyObject *igraphmodule_Graph_Forest_Fire(PyTypeObject * type, &n, &fw_prob, &bw_factor, &ambs, &directed)) return NULL; - CHECK_SSIZE_T_RANGE(n, "number of nodes"); + CHECK_SSIZE_T_RANGE(n, "vertex count"); CHECK_SSIZE_T_RANGE(n, "number of ambassadors"); if (igraph_forest_fire_game(&g, n, fw_prob, bw_factor, ambs, PyObject_IsTrue(directed))) { @@ -2430,7 +2430,7 @@ PyObject *igraphmodule_Graph_Full(PyTypeObject * type, if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OO", kwlist, &n, &directed, &loops)) return NULL; - CHECK_SSIZE_T_RANGE(n, "number of nodes"); + CHECK_SSIZE_T_RANGE(n, "vertex count"); if (igraph_full(&g, n, PyObject_IsTrue(directed), PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); @@ -2675,14 +2675,14 @@ PyObject *igraphmodule_Graph_Incidence(PyTypeObject * type, PyObject *igraphmodule_Graph_Isoclass(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long int n, isoclass; + Py_ssize_t n, isoclass; PyObject *directed = Py_False; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "cls", "directed", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|O", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|O", kwlist, &n, &isoclass, &directed)) return NULL; @@ -2692,8 +2692,7 @@ PyObject *igraphmodule_Graph_Isoclass(PyTypeObject * type, return NULL; } - if (igraph_isoclass_create(&g, (igraph_integer_t) n, - (igraph_integer_t) isoclass, PyObject_IsTrue(directed))) { + if (igraph_isoclass_create(&g, n, isoclass, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2709,15 +2708,19 @@ PyObject *igraphmodule_Graph_Isoclass(PyTypeObject * type, */ PyObject *igraphmodule_Graph_Kautz(PyTypeObject *type, PyObject *args, PyObject *kwds) { - long int m, n; + Py_ssize_t m, n; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = {"m", "n", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll", kwlist, &m, &n)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn", kwlist, &m, &n)) { return NULL; + } + + CHECK_SSIZE_T_RANGE(m, "m"); + CHECK_SSIZE_T_RANGE(n, "n"); - if (igraph_kautz(&g, (igraph_integer_t) m, (igraph_integer_t) n)) { + if (igraph_kautz(&g, m, n)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2737,17 +2740,19 @@ PyObject *igraphmodule_Graph_K_Regular(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long int n, k; + Py_ssize_t n, k; PyObject *directed_o = Py_False, *multiple_o = Py_False; static char *kwlist[] = { "n", "k", "directed", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|OO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|OO", kwlist, &n, &k, &directed_o, &multiple_o)) return NULL; - if (igraph_k_regular_game(&g, (igraph_integer_t) n, (igraph_integer_t) k, - PyObject_IsTrue(directed_o), PyObject_IsTrue(multiple_o))) { + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(k, "degree"); + + if (igraph_k_regular_game(&g, n, k, PyObject_IsTrue(directed_o), PyObject_IsTrue(multiple_o))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -2766,7 +2771,7 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, PyObject * args, PyObject * kwds) { igraph_vector_int_t dimvector; - long int nei = 1; + Py_ssize_t nei = 1; igraph_bool_t directed; igraph_bool_t mutual; igraph_bool_t circular; @@ -2777,7 +2782,7 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, static char *kwlist[] = { "dim", "nei", "directed", "mutual", "circular", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|lOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|nOOO", kwlist, &PyList_Type, &o_dimvector, &nei, &o_directed, &o_mutual, &o_circular)) return NULL; @@ -2789,8 +2794,9 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, if (igraphmodule_PyObject_to_vector_int_t(o_dimvector, &dimvector)) return NULL; - if (igraph_lattice(&g, &dimvector, (igraph_integer_t) nei, - directed, mutual, circular)) { + CHECK_SSIZE_T_RANGE_POSITIVE(nei, "number of neighbors"); + + if (igraph_lattice(&g, &dimvector, nei, directed, mutual, circular)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&dimvector); return NULL; @@ -2811,21 +2817,24 @@ PyObject *igraphmodule_Graph_Lattice(PyTypeObject * type, PyObject *igraphmodule_Graph_LCF(PyTypeObject *type, PyObject *args, PyObject *kwds) { igraph_vector_int_t shifts; - long int repeats, n; - PyObject *o_shifts; + Py_ssize_t repeats, n; + PyObject *shifts_o; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "shifts", "repeats", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lOl", kwlist, - &n, &o_shifts, &repeats)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOn", kwlist, + &n, &shifts_o, &repeats)) return NULL; - if (igraphmodule_PyObject_to_vector_int_t(o_shifts, &shifts)) + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(repeats, "repeat count"); + + if (igraphmodule_PyObject_to_vector_int_t(shifts_o, &shifts)) return NULL; - if (igraph_lcf_vector(&g, (igraph_integer_t) n, &shifts, (igraph_integer_t) repeats)) { + if (igraph_lcf_vector(&g, n, &shifts, repeats)) { igraph_vector_int_destroy(&shifts); igraphmodule_handle_igraph_error(); return NULL; @@ -2910,7 +2919,7 @@ PyObject *igraphmodule_Graph_Preference(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, types; + Py_ssize_t n, types; PyObject *type_dist, *pref_matrix; PyObject *directed = Py_False; PyObject *loops = Py_False; @@ -2925,12 +2934,13 @@ PyObject *igraphmodule_Graph_Preference(PyTypeObject * type, { "n", "type_dist", "pref_matrix", "attribute", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lO!O!|OOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OOO", kwlist, &n, &PyList_Type, &type_dist, &PyList_Type, &pref_matrix, &attribute_key, &directed, &loops)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); types = PyList_Size(type_dist); if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm)) return NULL; @@ -2940,15 +2950,14 @@ NULL }; } store_attribs = (attribute_key && attribute_key != Py_None); - if (store_attribs && igraph_vector_int_init(&type_vec, (igraph_integer_t) n)) { + if (store_attribs && igraph_vector_int_init(&type_vec, n)) { igraph_matrix_destroy(&pm); igraph_vector_destroy(&td); igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_preference_game(&g, (igraph_integer_t) n, - (igraph_integer_t) types, &td, 0, &pm, + if (igraph_preference_game(&g, n, types, &td, 0, &pm, store_attribs ? &type_vec : 0, PyObject_IsTrue(directed), PyObject_IsTrue(loops))) { @@ -3003,7 +3012,7 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, in_types, out_types; + Py_ssize_t n, in_types, out_types; PyObject *type_dist_matrix, *pref_matrix; PyObject *loops = Py_False; igraph_matrix_t pm; @@ -3016,12 +3025,14 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, char *kwlist[] = { "n", "type_dist_matrix", "pref_matrix", "attribute", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lO!O!|OO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OO", kwlist, &n, &PyList_Type, &type_dist_matrix, &PyList_Type, &pref_matrix, &attribute_key, &loops)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); + if (igraphmodule_PyList_to_matrix_t(pref_matrix, &pm)) return NULL; if (igraphmodule_PyList_to_matrix_t(type_dist_matrix, &td)) { igraph_matrix_destroy(&pm); @@ -3033,13 +3044,13 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, store_attribs = (attribute_key && attribute_key != Py_None); if (store_attribs) { - if (igraph_vector_int_init(&in_type_vec, (igraph_integer_t) n)) { + if (igraph_vector_int_init(&in_type_vec, n)) { igraph_matrix_destroy(&pm); igraph_matrix_destroy(&td); igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_vector_int_init(&out_type_vec, (igraph_integer_t) n)) { + if (igraph_vector_int_init(&out_type_vec, n)) { igraph_matrix_destroy(&pm); igraph_matrix_destroy(&td); igraph_vector_int_destroy(&in_type_vec); @@ -3048,10 +3059,7 @@ PyObject *igraphmodule_Graph_Asymmetric_Preference(PyTypeObject * type, } } - if (igraph_asymmetric_preference_game(&g, (igraph_integer_t) n, - (igraph_integer_t) in_types, - (igraph_integer_t) out_types, - &td, &pm, + if (igraph_asymmetric_preference_game(&g, n, in_types, out_types, &td, &pm, store_attribs ? &in_type_vec : 0, store_attribs ? &out_type_vec : 0, PyObject_IsTrue(loops))) { @@ -3109,7 +3117,7 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long int n1, n2, m = -1; + Py_ssize_t n1, n2, m = -1; double p = -1.0; igraph_erdos_renyi_t t; igraph_neimode_t neimode = IGRAPH_ALL; @@ -3119,10 +3127,13 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, static char *kwlist[] = { "n1", "n2", "p", "m", "directed", "neimode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|dlOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|dnOO", kwlist, &n1, &n2, &p, &m, &directed_o, &neimode_o)) return NULL; + CHECK_SSIZE_T_RANGE(n1, "number of vertices in first partition"); + CHECK_SSIZE_T_RANGE(n2, "number of vertices in second partition"); + if (m == -1 && p == -1.0) { /* no density parameters were given, throw exception */ PyErr_SetString(PyExc_TypeError, "Either m or p must be given."); @@ -3144,10 +3155,7 @@ PyObject *igraphmodule_Graph_Random_Bipartite(PyTypeObject * type, return NULL; } - if (igraph_bipartite_game(&g, &vertex_types, t, - (igraph_integer_t) n1, (igraph_integer_t) n2, - (igraph_real_t) p, (igraph_integer_t) m, - PyObject_IsTrue(directed_o), neimode)) { + if (igraph_bipartite_game(&g, &vertex_types, t, n1, n2, p, m, PyObject_IsTrue(directed_o), neimode)) { igraph_vector_bool_destroy(&vertex_types); igraphmodule_handle_igraph_error(); return NULL; @@ -3173,7 +3181,7 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long n, window = 0; + Py_ssize_t n, window = 0; float power = 0.0f, zero_appeal = 0.0f; igraph_integer_t m = 0; igraph_vector_int_t outseq; @@ -3183,15 +3191,13 @@ PyObject *igraphmodule_Graph_Recent_Degree(PyTypeObject * type, { "n", "m", "window", "outpref", "directed", "power", "zero_appeal", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lOl|OOff", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nOn|OOff", kwlist, &n, &m_obj, &window, &outpref, &directed, &power, &zero_appeal)) return NULL; - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); - return NULL; - } + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(window, "window size"); // let's check whether we have a constant out-degree or a list if (PyLong_Check(m_obj)) { @@ -3199,20 +3205,16 @@ NULL }; return NULL; } igraph_vector_int_init(&outseq, 0); - } - else if (PyList_Check(m_obj)) { + } else if (PyList_Check(m_obj)) { if (igraphmodule_PyObject_to_vector_int_t(m_obj, &outseq)) { // something bad happened during conversion return NULL; } } - if (igraph_recent_degree_game(&g, (igraph_integer_t) n, - (igraph_real_t) power, - (igraph_integer_t) window, - m, &outseq, + if (igraph_recent_degree_game(&g, n, power, window, m, &outseq, PyObject_IsTrue(outpref), - (igraph_real_t) zero_appeal, + zero_appeal, PyObject_IsTrue(directed))) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&outseq); @@ -3234,26 +3236,20 @@ NULL }; PyObject *igraphmodule_Graph_Ring(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long n; + Py_ssize_t n; PyObject *directed = Py_False, *mutual = Py_False, *circular = Py_True; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "directed", "mutual", "circular", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|O!O!O!", kwlist, &n, - &PyBool_Type, &directed, - &PyBool_Type, &mutual, - &PyBool_Type, &circular)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOO", kwlist, &n, + &directed, &mutual, &circular)) return NULL; - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); - return NULL; - } + CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraph_ring(&g, (igraph_integer_t) n, (directed == Py_True), - (mutual == Py_True), (circular == Py_True))) { + if (igraph_ring(&g, n, PyObject_IsTrue(directed), PyObject_IsTrue(mutual), PyObject_IsTrue(circular))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3273,7 +3269,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, { igraphmodule_GraphObject *self; igraph_t g; - long int n; + Py_ssize_t n; PyObject *block_sizes_o, *pref_matrix_o; PyObject *directed_o = Py_False; PyObject *loops_o = Py_False; @@ -3283,20 +3279,24 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, static char *kwlist[] = { "n", "pref_matrix", "block_sizes", "directed", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "lO!O!|OO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nO!O!|OO", kwlist, &n, &PyList_Type, &pref_matrix_o, &PyList_Type, &block_sizes_o, &directed_o, &loops_o)) return NULL; - if (igraphmodule_PyList_to_matrix_t(pref_matrix_o, &pref_matrix)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); + + if (igraphmodule_PyList_to_matrix_t(pref_matrix_o, &pref_matrix)) { + return NULL; + } + if (igraphmodule_PyObject_to_vector_int_t(block_sizes_o, &block_sizes)) { igraph_matrix_destroy(&pref_matrix); return NULL; } - if (igraph_sbm_game(&g, (igraph_integer_t) n, &pref_matrix, &block_sizes, - PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o))) { + if (igraph_sbm_game(&g, n, &pref_matrix, &block_sizes, PyObject_IsTrue(directed_o), PyObject_IsTrue(loops_o))) { igraphmodule_handle_igraph_error(); igraph_matrix_destroy(&pref_matrix); igraph_vector_int_destroy(&block_sizes); @@ -3307,6 +3307,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, igraph_vector_int_destroy(&block_sizes); CREATE_GRAPH_FROM_TYPE(self, g, type); + return (PyObject *) self; } @@ -3837,17 +3838,14 @@ PyObject *igraphmodule_Graph_average_path_length(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - char *kwlist[] = { "directed", "unconn", NULL }; + static char *kwlist[] = { "directed", "unconn", NULL }; PyObject *directed = Py_True, *unconn = Py_True; igraph_real_t res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!O!", kwlist, - &PyBool_Type, &directed, - &PyBool_Type, &unconn)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &directed, &unconn)) return NULL; - if (igraph_average_path_length(&self->g, &res, 0, (directed == Py_True), - (unconn == Py_True))) { + if (igraph_average_path_length(&self->g, &res, 0, PyObject_IsTrue(directed), PyObject_IsTrue(unconn))) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3914,7 +3912,7 @@ PyObject *igraphmodule_Graph_betweenness(igraphmodule_GraphObject * self, return NULL; } if (igraph_betweenness_cutoff(&self->g, &res, vs, PyObject_IsTrue(directed), - weights, (igraph_real_t)PyFloat_AsDouble(cutoff_num))) { + weights, PyFloat_AsDouble(cutoff_num))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4263,7 +4261,7 @@ PyObject *igraphmodule_Graph_closeness(igraphmodule_GraphObject * self, return NULL; } if (igraph_closeness_cutoff(&self->g, &res, 0, 0, vs, mode, weights, - (igraph_real_t)PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { + PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4343,7 +4341,7 @@ PyObject *igraphmodule_Graph_harmonic_centrality(igraphmodule_GraphObject * self return NULL; } if (igraph_harmonic_centrality_cutoff(&self->g, &res, vs, mode, weights, - (igraph_real_t)PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { + PyFloat_AsDouble(cutoff_num), PyObject_IsTrue(normalized_o))) { igraph_vs_destroy(&vs); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -4728,7 +4726,7 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, igraph_vector_destroy(&res); return NULL; } if (igraph_edge_betweenness_cutoff(&self->g, &res, PyObject_IsTrue(directed), - weights, (igraph_real_t)PyFloat_AsDouble(cutoff_num))) { + weights, PyFloat_AsDouble(cutoff_num))) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&res); if (weights) { igraph_vector_destroy(weights); free(weights); } @@ -10140,25 +10138,27 @@ PyObject *igraphmodule_Graph_maxflow_value(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - PyObject *capacity_object = Py_None; + PyObject *capacity_object = Py_None, *v1_o, *v2_o; igraph_vector_t capacity_vector; igraph_real_t result; - long int vid1 = -1, vid2 = -1; igraph_integer_t v1, v2; igraph_maxflow_stats_t stats; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|O", kwlist, - &vid1, &vid2, &capacity_object)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", kwlist, + &v1_o, &v2_o, &capacity_object)) + return NULL; + + if (igraphmodule_PyObject_to_vid(v1_o, &v1, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_vid(v2_o, &v2, &self->g)) return NULL; - v1 = (igraph_integer_t) vid1; - v2 = (igraph_integer_t) vid2; if (igraphmodule_PyObject_to_attribute_values(capacity_object, &capacity_vector, self, ATTRHASH_IDX_EDGE, 1.0)) return igraphmodule_handle_igraph_error(); - if (igraph_maxflow_value(&self->g, &result, v1, v2, &capacity_vector, &stats)) { igraph_vector_destroy(&capacity_vector); @@ -10177,21 +10177,24 @@ PyObject *igraphmodule_Graph_maxflow(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - PyObject *capacity_object = Py_None, *flow_o, *cut_o, *partition_o; + PyObject *capacity_object = Py_None, *flow_o, *cut_o, *partition_o, *v1_o, *v2_o; igraph_vector_t capacity_vector; igraph_real_t result; - long int vid1 = -1, vid2 = -1; igraph_integer_t v1, v2; igraph_vector_t flow; igraph_vector_int_t cut, partition; igraph_maxflow_stats_t stats; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|O", kwlist, - &vid1, &vid2, &capacity_object)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|O", kwlist, + &v1_o, &v2_o, &capacity_object)) + return NULL; + + if (igraphmodule_PyObject_to_vid(v1_o, &v1, &self->g)) + return NULL; + + if (igraphmodule_PyObject_to_vid(v2_o, &v2, &self->g)) return NULL; - v1 = (igraph_integer_t) vid1; - v2 = (igraph_integer_t) vid2; if (igraphmodule_PyObject_to_attribute_values(capacity_object, &capacity_vector, self, ATTRHASH_IDX_EDGE, 1.0)) @@ -10384,15 +10387,13 @@ PyObject *igraphmodule_Graph_mincut_value(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "source", "target", "capacity", NULL }; - PyObject *capacity_object = Py_None; + PyObject *capacity_object = Py_None, *v1_o = Py_None, *v2_o = Py_None; igraph_vector_t capacity_vector; igraph_real_t result, mincut; - igraph_integer_t v1, v2; - long vid1 = -1, vid2 = -1; - long n; + igraph_integer_t n, v1 = -1, v2 = -1; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|llO", kwlist, - &vid1, &vid2, &capacity_object)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, + &v1_o, &v2_o, &capacity_object)) return NULL; if (igraphmodule_PyObject_to_attribute_values(capacity_object, @@ -10400,20 +10401,24 @@ PyObject *igraphmodule_Graph_mincut_value(igraphmodule_GraphObject * self, self, ATTRHASH_IDX_EDGE, 1.0)) return igraphmodule_handle_igraph_error(); - v1 = (igraph_integer_t) vid1; - v2 = (igraph_integer_t) vid2; + if (v1_o != Py_None && igraphmodule_PyObject_to_vid(v1_o, &v1, &self->g)) + return NULL; + + if (v2_o != Py_None && igraphmodule_PyObject_to_vid(v2_o, &v2, &self->g)) + return NULL; + if (v1 == -1 && v2 == -1) { if (igraph_mincut_value(&self->g, &result, &capacity_vector)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } - } - else if (v1 == -1) { + } else if (v1 == -1) { n = igraph_vcount(&self->g); result = -1; for (v1 = 0; v1 < n; v1++) { - if (v2 == v1) + if (v2 == v1) { continue; + } if (igraph_st_mincut_value(&self->g, &mincut, v1, v2, &capacity_vector)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); @@ -10421,26 +10426,27 @@ PyObject *igraphmodule_Graph_mincut_value(igraphmodule_GraphObject * self, if (result < 0 || result > mincut) result = mincut; } - if (result < 0) + if (result < 0) { result = 0.0; - } - else if (v2 == -1) { + } + } else if (v2 == -1) { n = igraph_vcount(&self->g); result = -1; for (v2 = 0; v2 < n; v2++) { - if (v2 == v1) + if (v2 == v1) { continue; + } if (igraph_st_mincut_value(&self->g, &mincut, v1, v2, &capacity_vector)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); } - if (result < 0.0 || result > mincut) + if (result < 0.0 || result > mincut) { result = mincut; + } } if (result < 0) result = 0.0; - } - else { + } else { if (igraph_st_mincut_value(&self->g, &result, v1, v2, &capacity_vector)) { igraph_vector_destroy(&capacity_vector); return igraphmodule_handle_igraph_error(); @@ -11040,17 +11046,15 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, PyObject* args, PyObject* kwds) { static char* kwlist[] = { "min", "max", "file", NULL }; PyObject *list, *item, *file = Py_None; - long int i = 0, j = 0; - igraph_integer_t min, max; - Py_ssize_t n; + Py_ssize_t min = 0, max = 0, i, j, n; igraph_vector_ptr_t result; igraphmodule_filehandle_t filehandle; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|llO", kwlist, &i, &j, &file)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min, &max, &file)) return NULL; - min = (igraph_integer_t) i; - max = (igraph_integer_t) j; + CHECK_SSIZE_T_RANGE(min, "minimum clique size"); + CHECK_SSIZE_T_RANGE(max, "maximum clique size"); if (file == Py_None) { if (igraph_vector_ptr_init(&result, 0)) { @@ -11063,10 +11067,11 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, return igraphmodule_handle_igraph_error(); } - n = (Py_ssize_t)igraph_vector_ptr_size(&result); + n = igraph_vector_ptr_size(&result); list = PyList_New(n); - if (!list) + if (!list) { return NULL; + } for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 8c08de894..f25b82be1 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -284,8 +284,8 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_matrix_destroy(&mtrx); return NULL; } - MATRIX(mtrx, i, 0)=(igraph_real_t)PyFloat_AsDouble(o1); - MATRIX(mtrx, i, 1)=(igraph_real_t)PyFloat_AsDouble(o2); + MATRIX(mtrx, i, 0) = PyFloat_AsDouble(o1); + MATRIX(mtrx, i, 1) = PyFloat_AsDouble(o2); Py_DECREF(o1); Py_DECREF(o2); } From 4195609e6ac7f0bce3f8d41774b7ce563eb7505b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 22:16:01 +0200 Subject: [PATCH 0387/1681] refactor: getting rid of even more (igraph_integer_t) conversions --- src/_igraph/graphobject.c | 94 ++++++++++++++++++--------------------- tests/test_generators.py | 4 +- 2 files changed, 45 insertions(+), 53 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 4ef1dce6e..8d0d2e416 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3319,7 +3319,7 @@ PyObject *igraphmodule_Graph_SBM(PyTypeObject * type, PyObject *igraphmodule_Graph_Star(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long n, center = 0; + Py_ssize_t n, center = 0; igraph_star_mode_t mode = IGRAPH_STAR_UNDIRECTED; PyObject* mode_o = Py_None; igraphmodule_GraphObject *self; @@ -3327,28 +3327,22 @@ PyObject *igraphmodule_Graph_Star(PyTypeObject * type, static char *kwlist[] = { "n", "mode", "center", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|Ol", kwlist, - &n, &mode_o, ¢er)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|On", kwlist, &n, &mode_o, ¢er)) return NULL; - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); - return NULL; - } + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(center, "central vertex ID"); - if (center >= n || center < 0) { - PyErr_SetString(PyExc_ValueError, - "Central vertex ID should be between 0 and n-1"); + if (center >= n) { + PyErr_SetString(PyExc_ValueError, "central vertex ID should be between 0 and n-1"); return NULL; } if (igraphmodule_PyObject_to_star_mode_t(mode_o, &mode)) { - PyErr_SetString(PyExc_ValueError, - "Mode should be either \"in\", \"out\", \"mutual\" or \"undirected\""); return NULL; } - if (igraph_star(&g, (igraph_integer_t) n, mode, (igraph_integer_t) center)) { + if (igraph_star(&g, n, mode, center)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3368,7 +3362,7 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, PyObject* args, PyObject* kwds) { igraphmodule_GraphObject *self; igraph_t g; - long int m; + Py_ssize_t m; PyObject *fitness_out_o = Py_None, *fitness_in_o = Py_None; PyObject *fitness_o = Py_None; PyObject *multiple = Py_False, *loops = Py_False; @@ -3377,11 +3371,13 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, static char *kwlist[] = { "m", "fitness_out", "fitness_in", "loops", "multiple", "fitness", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|OOOOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OOOOO", kwlist, &m, &fitness_out_o, &fitness_in_o, &loops, &multiple, &fitness_o)) return NULL; + CHECK_SSIZE_T_RANGE(m, "edge count"); + /* This trickery allows us to use "fitness" or "fitness_out" as * keyword argument, with "fitness_out" taking precedence over * "fitness" */ @@ -3403,7 +3399,7 @@ PyObject *igraphmodule_Graph_Static_Fitness(PyTypeObject *type, } } - if (igraph_static_fitness_game(&g, (igraph_integer_t) m, &fitness_out, + if (igraph_static_fitness_game(&g, m, &fitness_out, fitness_in_o == Py_None ? 0 : &fitness_in, PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { igraph_vector_destroy(&fitness_out); @@ -3431,7 +3427,7 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, PyObject* args, PyObject* kwds) { igraphmodule_GraphObject *self; igraph_t g; - long int n, m; + Py_ssize_t n, m; float exponent_out = -1.0f, exponent_in = -1.0f, exponent = -1.0f; PyObject *multiple = Py_False, *loops = Py_False; PyObject *finite_size_correction = Py_True; @@ -3439,12 +3435,15 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, static char *kwlist[] = { "n", "m", "exponent_out", "exponent_in", "loops", "multiple", "finite_size_correction", "exponent", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|ffOOOf", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|ffOOOf", kwlist, &n, &m, &exponent_out, &exponent_in, &loops, &multiple, &finite_size_correction, &exponent)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(m, "edge count"); + /* This trickery allows us to use "exponent" or "exponent_out" as * keyword argument, with "exponent_out" taking precedence over * "exponent" */ @@ -3456,8 +3455,7 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, return NULL; } - if (igraph_static_power_law_game(&g, (igraph_integer_t) n, (igraph_integer_t) m, - exponent_out, exponent_in, + if (igraph_static_power_law_game(&g, n, m, exponent_out, exponent_in, PyObject_IsTrue(loops), PyObject_IsTrue(multiple), PyObject_IsTrue(finite_size_correction))) { igraphmodule_handle_igraph_error(); @@ -3465,6 +3463,7 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, } CREATE_GRAPH_FROM_TYPE(self, g, type); + return (PyObject *) self; } @@ -3476,34 +3475,31 @@ PyObject *igraphmodule_Graph_Static_Power_Law(PyTypeObject *type, PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long int n, children; - PyObject *tree_mode_o = Py_None, *tree_type_o = Py_None; + Py_ssize_t n, children; + PyObject *tree_mode_o = Py_None; igraph_tree_mode_t mode = IGRAPH_TREE_UNDIRECTED; igraphmodule_GraphObject *self; igraph_t g; - static char *kwlist[] = { "n", "children", "mode", "type", NULL }; + static char *kwlist[] = { "n", "children", "mode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ll|OO", kwlist, - &n, &children, - &tree_mode_o, &tree_type_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nn|O", kwlist, + &n, &children, &tree_mode_o)) return NULL; + CHECK_SSIZE_T_RANGE(n, "vertex count"); + CHECK_SSIZE_T_RANGE(children, "number of children per vertex"); + if (n < 0) { PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); return NULL; } - if (tree_mode_o == Py_None && tree_type_o != Py_None) { - tree_mode_o = tree_type_o; - PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); - } - if (igraphmodule_PyObject_to_tree_mode_t(tree_mode_o, &mode)) { return NULL; } - if (igraph_tree(&g, (igraph_integer_t) n, (igraph_integer_t) children, mode)) { + if (igraph_tree(&g, n, children, mode)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3531,31 +3527,25 @@ PyObject *igraphmodule_Graph_Tree(PyTypeObject * type, PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long int n; - PyObject *directed_o = Py_False, *tree_method_o = Py_None; - igraph_bool_t directed; + Py_ssize_t n; + PyObject *directed = Py_False, *tree_method_o = Py_None; igraph_random_tree_t tree_method = IGRAPH_RANDOM_TREE_LERW; igraphmodule_GraphObject *self; igraph_t g; static char *kwlist[] = { "n", "directed", "method", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|OO", kwlist, - &n, &directed_o, &tree_method_o)) - return NULL; - - if (n < 0) { - PyErr_SetString(PyExc_ValueError, "Number of vertices must be positive."); + if (!PyArg_ParseTupleAndKeywords(args, kwds, "n|OO", kwlist, + &n, &directed, &tree_method_o)) return NULL; - } - directed = PyObject_IsTrue(directed_o); + CHECK_SSIZE_T_RANGE(n, "vertex count"); - if (igraphmodule_PyObject_to_random_tree_t(tree_method_o, &tree_method)) + if (igraphmodule_PyObject_to_random_tree_t(tree_method_o, &tree_method)) { return NULL; + } - if (igraph_tree_game(&g, (igraph_integer_t) n, directed, - tree_method)) { + if (igraph_tree_game(&g, n, PyObject_IsTrue(directed), tree_method)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -3573,7 +3563,7 @@ PyObject *igraphmodule_Graph_Tree_Game(PyTypeObject * type, PyObject *igraphmodule_Graph_Watts_Strogatz(PyTypeObject * type, PyObject * args, PyObject * kwds) { - long int nei = 1, dim, size; + Py_ssize_t dim, size, nei; double p; PyObject* loops = Py_False; PyObject* multiple = Py_False; @@ -3582,13 +3572,15 @@ PyObject *igraphmodule_Graph_Watts_Strogatz(PyTypeObject * type, static char *kwlist[] = { "dim", "size", "nei", "p", "loops", "multiple", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "llld|OO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "nnnd|OO", kwlist, &dim, &size, &nei, &p, &loops, &multiple)) return NULL; - if (igraph_watts_strogatz_game(&g, (igraph_integer_t) dim, - (igraph_integer_t) size, (igraph_integer_t) nei, p, - PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { + CHECK_SSIZE_T_RANGE(dim, "dimensionality"); + CHECK_SSIZE_T_RANGE(size, "size"); + CHECK_SSIZE_T_RANGE(nei, "number of neighbors"); + + if (igraph_watts_strogatz_game(&g, dim, size, nei, p, PyObject_IsTrue(loops), PyObject_IsTrue(multiple))) { igraphmodule_handle_igraph_error(); return NULL; } diff --git a/tests/test_generators.py b/tests/test_generators.py index ac55a3b78..c7963a4b5 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -133,7 +133,7 @@ def testLCF(self): g1 = Graph.LCF(12, (5, -5), 6) g2 = Graph.Famous("Franklin") self.assertTrue(g1.isomorphic(g2)) - self.assertRaises(InternalError, Graph.LCF, 12, (5, -5), -3) + self.assertRaises(ValueError, Graph.LCF, 12, (5, -5), -3) def testRealizeDegreeSequence(self): # Test case insensitivity of options too @@ -229,7 +229,7 @@ def testSBM(self): self.assertTrue(sum(g.is_loop()) == 0) # Check error conditions - self.assertRaises(InternalError, Graph.SBM, -1, pref_matrix, types) + self.assertRaises(ValueError, Graph.SBM, -1, pref_matrix, types) self.assertRaises(InternalError, Graph.SBM, 61, pref_matrix, types) pref_matrix[0][1] = 0.7 self.assertRaises(InternalError, Graph.SBM, 60, pref_matrix, types) From 1eeb41fed06ec883b0aff03bdc57e02d23a7eb48 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 9 Sep 2021 22:24:32 +0200 Subject: [PATCH 0388/1681] refactor: igraphmodule_convex_hull does not use long any more --- src/_igraph/igraphmodule.c | 78 +++++++++++++++++++-------------- test.sh | 20 +++++++++ warnings_converted_to_errors.py | 11 +++++ 3 files changed, 77 insertions(+), 32 deletions(-) create mode 100755 test.sh create mode 100644 warnings_converted_to_errors.py diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index f25b82be1..34557347f 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -230,64 +230,78 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) { PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwds) { static char* kwlist[] = {"vs", "coords", NULL}; - PyObject *vs, *o, *o1=0, *o2=0, *coords = Py_False; + PyObject *vs, *o, *o1, *o2, *o1_float, *o2_float, *coords = Py_False; igraph_matrix_t mtrx; igraph_vector_int_t result; igraph_matrix_t resmat; - long no_of_nodes, i; + Py_ssize_t no_of_nodes, i; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, &PyList_Type, &vs, &coords)) return NULL; - no_of_nodes=PyList_Size(vs); + no_of_nodes = PyList_Size(vs); if (igraph_matrix_init(&mtrx, no_of_nodes, 2)) { igraphmodule_handle_igraph_error(); return NULL; } - for (i=0; i= 2) { - o1=PyList_GetItem(o, 0); - o2=PyList_GetItem(o, 1); - if (PyList_Size(o) > 2) + + for (i = 0; i < no_of_nodes; i++) { + o = PyList_GetItem(vs, i); + + if (PySequence_Check(o)) { + if (PySequence_Size(o) >= 2) { + o1 = PySequence_GetItem(o, 0); + if (!o1) { + igraph_matrix_destroy(&mtrx); + return NULL; + } + + o2 = PySequence_GetItem(o, 1); + if (!o2) { + Py_DECREF(o1); + igraph_matrix_destroy(&mtrx); + return NULL; + } + + if (PySequence_Size(o) > 2) { PyErr_Warn(PyExc_Warning, "vertex with more than 2 coordinates found, considering only the first 2"); + } } else { PyErr_SetString(PyExc_TypeError, "vertex with less than 2 coordinates found"); igraph_matrix_destroy(&mtrx); - return NULL; - } - } else if (PyTuple_Check(o)) { - if (PyTuple_Size(o) >= 2) { - o1=PyTuple_GetItem(o, 0); - o2=PyTuple_GetItem(o, 1); - if (PyTuple_Size(o) > 2) - PyErr_Warn(PyExc_Warning, "vertex with more than 2 coordinates found, considering only the first 2"); - } else { - PyErr_SetString(PyExc_TypeError, "vertex with less than 2 coordinates found"); - igraph_matrix_destroy(&mtrx); - return NULL; + return NULL; } } if (!PyNumber_Check(o1) || !PyNumber_Check(o2)) { PyErr_SetString(PyExc_TypeError, "vertex coordinates must be numeric"); + Py_DECREF(o2); + Py_DECREF(o1); igraph_matrix_destroy(&mtrx); return NULL; } - /* o, o1 and o2 were borrowed, but now o1 and o2 are actual references! */ - o1=PyNumber_Float(o1); o2=PyNumber_Float(o2); - if (!o1 || !o2) { - PyErr_SetString(PyExc_TypeError, "vertex coordinate conversion to float failed"); - Py_XDECREF(o1); - Py_XDECREF(o2); + + o1_float = PyNumber_Float(o1); + if (!o1_float) { + Py_DECREF(o2); + Py_DECREF(o1); igraph_matrix_destroy(&mtrx); return NULL; } - MATRIX(mtrx, i, 0) = PyFloat_AsDouble(o1); - MATRIX(mtrx, i, 1) = PyFloat_AsDouble(o2); Py_DECREF(o1); + + o2_float = PyNumber_Float(o2); + if (!o2_float) { + Py_DECREF(o2); + igraph_matrix_destroy(&mtrx); + return NULL; + } Py_DECREF(o2); + + MATRIX(mtrx, i, 0) = PyFloat_AsDouble(o1_float); + MATRIX(mtrx, i, 1) = PyFloat_AsDouble(o2_float); + Py_DECREF(o1_float); + Py_DECREF(o2_float); } if (!PyObject_IsTrue(coords)) { @@ -302,7 +316,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_vector_int_destroy(&result); return NULL; } - o=igraphmodule_vector_int_t_to_PyList(&result); + o = igraphmodule_vector_int_t_to_PyList(&result); igraph_vector_int_destroy(&result); } else { if (igraph_matrix_init(&resmat, 0, 0)) { @@ -316,7 +330,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd igraph_matrix_destroy(&resmat); return NULL; } - o=igraphmodule_matrix_t_to_PyList(&resmat, IGRAPHMODULE_TYPE_FLOAT); + o = igraphmodule_matrix_t_to_PyList(&resmat, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&resmat); } diff --git a/test.sh b/test.sh new file mode 100755 index 000000000..007cc8cef --- /dev/null +++ b/test.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +VENV_DIR=.venv +PYTHON=python3 + +############################################################################### + +set -e + +if [ ! -d $VENV_DIR ]; then + $PYTHON -m venv $VENV_DIR + $VENV_DIR/bin/pip install -U pip wheel +fi + +rm -rf build/ +$VENV_DIR/bin/python setup.py build +$VENV_DIR/bin/pip install --use-feature=in-tree-build .[plotting,test] +$VENV_DIR/bin/pytest tests + + diff --git a/warnings_converted_to_errors.py b/warnings_converted_to_errors.py new file mode 100644 index 000000000..76ee5230b --- /dev/null +++ b/warnings_converted_to_errors.py @@ -0,0 +1,11 @@ +from igraph import Graph +import warnings + +warnings.simplefilter('error', 'Cannot shuffle graph') +degseq = [1,2,2,3] +try: + testgraph = Graph.Degree_Sequence(degseq,method = "vl") +except RuntimeWarning: + print(degseq) +else: + print("go on") From 2b4f56f060ff35a9e290792da67c51355b29398f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 00:41:49 +0200 Subject: [PATCH 0389/1681] refactor: all conversions from Python objects to long int are replaced with Py_ssize_t now --- src/_igraph/convert.c | 6 +- src/_igraph/graphobject.c | 481 ++++++++++++++++++++++++-------------- 2 files changed, 304 insertions(+), 183 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 5b75dac96..ca14f4e24 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2748,12 +2748,14 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g *vid = 0; } else if (PyLong_Check(o)) { /* Single vertex ID */ - if (igraphmodule_PyObject_to_integer_t(o, vid)) + if (igraphmodule_PyObject_to_integer_t(o, vid)) { return 1; + } } else if (graph != 0 && PyBaseString_Check(o)) { /* Single vertex ID from vertex name */ - if (igraphmodule_get_vertex_id_by_name(graph, o, vid)) + if (igraphmodule_get_vertex_id_by_name(graph, o, vid)) { return 1; + } } else if (PyObject_IsInstance(o, (PyObject*)&igraphmodule_VertexType)) { /* Single vertex ID from Vertex object */ igraphmodule_VertexObject *vo = (igraphmodule_VertexObject*)o; diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 8d0d2e416..58eab13b2 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3688,7 +3688,7 @@ PyObject *igraphmodule_Graph_assortativity_nominal(igraphmodule_GraphObject *sel static char *kwlist[] = { "types", "directed", NULL }; PyObject *types_o = Py_None, *directed = Py_True; igraph_real_t res; - int ret; + igraph_error_t ret; igraph_vector_int_t *types = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &types_o, &directed)) @@ -3720,7 +3720,7 @@ PyObject *igraphmodule_Graph_assortativity(igraphmodule_GraphObject *self, PyObj static char *kwlist[] = { "types1", "types2", "directed", NULL }; PyObject *types1_o = Py_None, *types2_o = Py_None, *directed = Py_True; igraph_real_t res; - int ret; + igraph_error_t ret; igraph_vector_t *types1 = 0, *types2 = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, &types1_o, &types2_o, &directed)) @@ -4041,18 +4041,28 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel igraph_vector_int_t multiplicities[2]; igraph_t g1, g2; igraph_t *p_g1 = &g1, *p_g2 = &g2; - long int probe1 = -1; - long int which = -1; + Py_ssize_t probe1 = -1, which = -1; static char* kwlist[] = {"types", "multiplicity", "probe1", "which", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|Oll", kwlist, &types_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|Onn", kwlist, &types_o, &multiplicity_o, &probe1, &which)) return NULL; if (igraphmodule_attrib_to_vector_bool_t(types_o, self, &types, ATTRIBUTE_TYPE_VERTEX)) return NULL; + if (which >= 0) { + CHECK_SSIZE_T_RANGE(which, "'which'"); + } else { + which = -1; + } + if (probe1 >= 0) { + CHECK_SSIZE_T_RANGE(probe1, "'probe1'"); + } else { + probe1 = -1; + } + if (which == 0) { p_g2 = 0; } else if (which == 1) { @@ -4076,7 +4086,7 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel if (igraph_bipartite_projection(&self->g, types, p_g1, p_g2, p_g1 ? &multiplicities[0] : 0, p_g2 ? &multiplicities[1] : 0, - (igraph_integer_t) probe1)) { + probe1)) { igraph_vector_int_destroy(&multiplicities[0]); igraph_vector_int_destroy(&multiplicities[1]); if (types) { igraph_vector_bool_destroy(types); free(types); } @@ -4084,8 +4094,7 @@ PyObject *igraphmodule_Graph_bipartite_projection(igraphmodule_GraphObject * sel return NULL; } } else { - if (igraph_bipartite_projection(&self->g, types, p_g1, p_g2, 0, 0, - (igraph_integer_t) probe1)) { + if (igraph_bipartite_projection(&self->g, types, p_g1, p_g2, 0, 0, probe1)) { if (types) { igraph_vector_bool_destroy(types); free(types); } igraphmodule_handle_igraph_error(); return NULL; @@ -4542,14 +4551,27 @@ PyObject *igraphmodule_Graph_decompose(igraphmodule_GraphObject * self, igraph_connectedness_t mode = IGRAPH_STRONG; PyObject *list, *mode_o = Py_None; igraphmodule_GraphObject *o; - long maxcompno = -1, minelements = -1, n, i; + Py_ssize_t maxcompno = -1, minelements = -1; + igraph_integer_t i, n; igraph_vector_ptr_t components; igraph_t *g; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oll", kwlist, &mode_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Onn", kwlist, &mode_o, &maxcompno, &minelements)) return NULL; + if (maxcompno >= 0) { + CHECK_SSIZE_T_RANGE(maxcompno, "maximum number of components"); + } else { + maxcompno = -1; + } + + if (minelements >= 0) { + CHECK_SSIZE_T_RANGE(minelements, "minimum number of vertices per component"); + } else { + minelements = -1; + } + if (igraphmodule_PyObject_to_connectedness_t(mode_o, &mode)) return NULL; @@ -4747,33 +4769,37 @@ PyObject *igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_edge_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", NULL }; - PyObject *checks = Py_True; - long int source = -1, target = -1, result; + PyObject *checks = Py_True, *source_o = Py_None, *target_o = Py_None; + igraph_integer_t source = -1, target = -1; igraph_integer_t res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|llO", kwlist, - &source, &target, &checks)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &source_o, &target_o, &checks)) + return NULL; + + if (source_o != Py_None && igraphmodule_PyObject_to_vid(source_o, &source, &self->g)) { + return NULL; + } + + if (target_o != Py_None && igraphmodule_PyObject_to_vid(target_o, &target, &self->g)) { return NULL; + } if (source < 0 && target < 0) { if (igraph_edge_connectivity(&self->g, &res, PyObject_IsTrue(checks))) { igraphmodule_handle_igraph_error(); - return NULL; + return NULL; } } else if (source >= 0 && target >= 0) { - if (igraph_st_edge_connectivity(&self->g, &res, (igraph_integer_t) source, - (igraph_integer_t) target)) { + if (igraph_st_edge_connectivity(&self->g, &res, source, target)) { igraphmodule_handle_igraph_error(); return NULL; } } else { - PyErr_SetString(PyExc_ValueError, "if source or target is given, the other one must also be specified"); - return NULL; + PyErr_SetString(PyExc_ValueError, "if source or target is given, the other one must also be specified"); + return NULL; } - result = res; - - return Py_BuildValue("l", result); + return igraphmodule_integer_t_to_PyObject(res); } /** \ingroup python_interface_graph @@ -5232,19 +5258,22 @@ PyObject *igraphmodule_Graph_neighborhood(igraphmodule_GraphObject *self, PyObject *vobj = Py_None; PyObject *mode_o = 0; PyObject *result; - long int order = 1; - int mindist = 0; + Py_ssize_t order = 1, mindist = 0; igraph_neimode_t mode = IGRAPH_ALL; igraph_bool_t return_single = 0; igraph_vs_t vs; igraph_vector_ptr_t res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlOi", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OnOn", kwlist, &vobj, &order, &mode_o, &mindist)) return NULL; - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + CHECK_SSIZE_T_RANGE(order, "neighborhood order"); + CHECK_SSIZE_T_RANGE(mindist, "minimum distance"); + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { return NULL; + } if (igraphmodule_PyObject_to_vs_t(vobj, &vs, &self->g, &return_single, 0)) { return igraphmodule_handle_igraph_error(); @@ -5255,18 +5284,18 @@ PyObject *igraphmodule_Graph_neighborhood(igraphmodule_GraphObject *self, return igraphmodule_handle_igraph_error(); } - if (igraph_neighborhood(&self->g, &res, vs, (igraph_integer_t) order, mode, - mindist)) { + if (igraph_neighborhood(&self->g, &res, vs, order, mode, mindist)) { igraph_vs_destroy(&vs); return igraphmodule_handle_igraph_error(); } igraph_vs_destroy(&vs); - if (!return_single) + if (!return_single) { result = igraphmodule_vector_int_ptr_t_to_PyList(&res); - else + } else { result = igraphmodule_vector_int_t_to_PyList((igraph_vector_int_t*)VECTOR(res)[0]); + } IGRAPH_VECTOR_PTR_SET_ITEM_DESTRUCTOR(&res, igraph_vector_int_destroy); igraph_vector_ptr_destroy_all(&res); @@ -5286,19 +5315,22 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, PyObject *vobj = Py_None; PyObject *mode_o = 0; PyObject *result; - long int order = 1; - int mindist = 0; + Py_ssize_t order = 1, mindist = 0; igraph_neimode_t mode = IGRAPH_ALL; igraph_bool_t return_single = 0; igraph_vs_t vs; igraph_vector_int_t res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlOi", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OnOn", kwlist, &vobj, &order, &mode_o, &mindist)) return NULL; - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + CHECK_SSIZE_T_RANGE(order, "neighborhood order"); + CHECK_SSIZE_T_RANGE(mindist, "minimum distance"); + + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { return NULL; + } if (igraphmodule_PyObject_to_vs_t(vobj, &vs, &self->g, &return_single, 0)) { return igraphmodule_handle_igraph_error(); @@ -5309,8 +5341,7 @@ PyObject *igraphmodule_Graph_neighborhood_size(igraphmodule_GraphObject *self, return igraphmodule_handle_igraph_error(); } - if (igraph_neighborhood_size(&self->g, &res, vs, (igraph_integer_t) order, mode, - mindist)) { + if (igraph_neighborhood_size(&self->g, &res, vs, order, mode, mindist)) { igraph_vs_destroy(&vs); return igraphmodule_handle_igraph_error(); } @@ -5352,20 +5383,22 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel igraph_vs_t vs, reset_vs; igraph_pagerank_algo_t algo=IGRAPH_PAGERANK_ALGO_PRPACK; PyObject *algo_o = Py_None; - long niter=1000; + Py_ssize_t niter = 1000; float eps=0.001f; void *opts; - int retval; + igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOOOO!Olf", kwlist, &vobj, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOOOO!Onf", kwlist, &vobj, &directed, &damping, &robj, - &rvsobj, &wobj, + &rvsobj, &wobj, &igraphmodule_ARPACKOptionsType, &arpack_options_o, &algo_o, &niter, &eps)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(niter, "number of iterations"); + if (robj != Py_None && rvsobj != Py_None) { PyErr_SetString(PyExc_ValueError, "only reset or reset_vs can be defined, not both"); return NULL; @@ -5516,17 +5549,20 @@ PyObject *igraphmodule_Graph_rewire(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "n", "mode", NULL }; - long int n = 1000; + Py_ssize_t n = 1000; PyObject *mode_o = Py_None; igraph_rewiring_t mode = IGRAPH_REWIRING_SIMPLE; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lO", kwlist, &n, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nO", kwlist, &n, &mode_o)) return NULL; - if (igraphmodule_PyObject_to_rewiring_t(mode_o, &mode)) + CHECK_SSIZE_T_RANGE(n, "number of rewiring attempts"); + + if (igraphmodule_PyObject_to_rewiring_t(mode_o, &mode)) { return NULL; + } - if (igraph_rewire(&self->g, (igraph_integer_t) n, mode)) { + if (igraph_rewire(&self->g, n, mode)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -6121,7 +6157,7 @@ PyObject igraph_bool_t return_single = 0; igraph_vs_t vs; igraph_transitivity_mode_t mode = IGRAPH_TRANSITIVITY_NAN; - int retval; + igraph_error_t retval; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &vobj, &mode_o, &weights_o)) return NULL; @@ -6188,7 +6224,7 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * igraph_neimode_t mode = IGRAPH_OUT; igraph_vector_int_t result; igraph_warning_handler_t* old_handler = 0; - int retval; + igraph_error_t retval; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &mode_o, &warnings_o)) return NULL; @@ -6229,25 +6265,32 @@ PyObject *igraphmodule_Graph_topological_sorting(igraphmodule_GraphObject * PyObject *igraphmodule_Graph_vertex_connectivity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "source", "target", "checks", "neighbors", NULL }; - PyObject *checks = Py_True, *neis = Py_None; - long int source = -1, target = -1; - igraph_integer_t res; + PyObject *checks = Py_True, *neis = Py_None, *source_o = Py_None, *target_o = Py_None; + igraph_integer_t source = -1, target = -1, res; igraph_vconn_nei_t neighbors = IGRAPH_VCONN_NEI_ERROR; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|llOO", kwlist, - &source, &target, &checks, &neis)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &source_o, &target_o, &checks, &neis)) return NULL; + if (source_o != Py_None && igraphmodule_PyObject_to_vid(source_o, &source, &self->g)) { + return NULL; + } + + if (target_o != Py_None && igraphmodule_PyObject_to_vid(target_o, &target, &self->g)) { + return NULL; + } + if (source < 0 && target < 0) { if (igraph_vertex_connectivity(&self->g, &res, PyObject_IsTrue(checks))) { igraphmodule_handle_igraph_error(); return NULL; } } else if (source >= 0 && target >= 0) { - if (igraphmodule_PyObject_to_vconn_nei_t(neis, &neighbors)) + if (igraphmodule_PyObject_to_vconn_nei_t(neis, &neighbors)) { return NULL; - if (igraph_st_vertex_connectivity(&self->g, &res, - (igraph_integer_t) source, (igraph_integer_t) target, neighbors)) { + } + if (igraph_st_vertex_connectivity(&self->g, &res, source, target, neighbors)) { igraphmodule_handle_igraph_error(); return NULL; } @@ -6378,16 +6421,18 @@ igraph_error_t igraphmodule_i_Graph_motifs_randesu_callback(const igraph_t *grap PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t result, cut_prob; - long int size=3; + Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; PyObject* callback=Py_None; PyObject *list; static char* kwlist[] = {"size", "cut_prob", "callback", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOO", kwlist, &size, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOO", kwlist, &size, &cut_prob_list, &callback)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(size, "motif size"); + if (cut_prob_list == Py_None) { if (igraph_vector_init(&cut_prob, size)) { return igraphmodule_handle_igraph_error(); @@ -6403,7 +6448,7 @@ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, igraph_vector_destroy(&cut_prob); return igraphmodule_handle_igraph_error(); } - if (igraph_motifs_randesu(&self->g, &result, (igraph_integer_t) size, &cut_prob)) { + if (igraph_motifs_randesu(&self->g, &result, size, &cut_prob)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&result); igraph_vector_destroy(&cut_prob); @@ -6419,7 +6464,7 @@ PyObject *igraphmodule_Graph_motifs_randesu(igraphmodule_GraphObject *self, igraphmodule_i_Graph_motifs_randesu_callback_data_t data; data.graph = (PyObject*)self; data.func = callback; - if (igraph_motifs_randesu_callback(&self->g, (igraph_integer_t) size, &cut_prob, + if (igraph_motifs_randesu_callback(&self->g, size, &cut_prob, igraphmodule_i_Graph_motifs_randesu_callback, &data)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&cut_prob); @@ -6445,13 +6490,15 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; igraph_integer_t result; - long int size=3; + Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; static char* kwlist[] = {"size", "cut_prob", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lO", kwlist, &size, &cut_prob_list)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nO", kwlist, &size, &cut_prob_list)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(size, "motif size"); + if (cut_prob_list == Py_None) { if (igraph_vector_init(&cut_prob, size)) { return igraphmodule_handle_igraph_error(); @@ -6462,7 +6509,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_no(igraphmodule_GraphObject *self, return NULL; } } - if (igraph_motifs_randesu_no(&self->g, &result, (igraph_integer_t) size, &cut_prob)) { + if (igraph_motifs_randesu_no(&self->g, &result, size, &cut_prob)) { igraphmodule_handle_igraph_error(); igraph_vector_destroy(&cut_prob); return NULL; @@ -6481,15 +6528,17 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s PyObject *args, PyObject *kwds) { igraph_vector_t cut_prob; igraph_integer_t result; - long size=3; + Py_ssize_t size = 3; PyObject* cut_prob_list=Py_None; PyObject *sample=Py_None; static char* kwlist[] = {"size", "cut_prob", "sample", NULL}; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOO", kwlist, &size, &cut_prob_list, &sample)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(size, "motif size"); + if (sample == Py_None) { PyErr_SetString(PyExc_TypeError, "sample size must be given"); return NULL; @@ -6525,7 +6574,7 @@ PyObject *igraphmodule_Graph_motifs_randesu_estimate(igraphmodule_GraphObject *s igraph_vector_destroy(&cut_prob); return NULL; } - if (igraph_motifs_randesu_estimate(&self->g, &result, (igraph_integer_t) size, + if (igraph_motifs_randesu_estimate(&self->g, &result, size, &cut_prob, 0, &samp)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&samp); @@ -6576,16 +6625,17 @@ PyObject *igraphmodule_Graph_layout_circle(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { igraph_matrix_t m; - int ret; - long dim = 2; + igraph_error_t ret; + Py_ssize_t dim = 2; PyObject *result; PyObject *order_o = Py_None; igraph_vs_t order; static char *kwlist[] = { "dim", "order", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lO", kwlist, &dim, &order_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nO", kwlist, &dim, &order_o)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(dim, "number of dimensions"); if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); return NULL; @@ -6636,14 +6686,15 @@ PyObject *igraphmodule_Graph_layout_random(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { igraph_matrix_t m; - int ret; - long dim = 2; + igraph_error_t ret; + Py_ssize_t dim = 2; PyObject *result; static char *kwlist[] = { "dim", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|l", kwlist, &dim)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|n", kwlist, &dim)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(dim, "number of dimensions"); if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); return NULL; @@ -6680,32 +6731,39 @@ PyObject *igraphmodule_Graph_layout_grid(igraphmodule_GraphObject* self, igraph_matrix_t m; PyObject *result; - long int width = 0, height = 0, dim = 2; - int ret; + Py_ssize_t width = 0, height = 0, dim = 2; + igraph_error_t ret; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lll", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnn", kwlist, &width, &height, &dim)) return NULL; - if (dim == 2 && height > 0) { - PyErr_SetString(PyExc_ValueError, "height must not be given if dim=2"); - return NULL; - } - + CHECK_SSIZE_T_RANGE_POSITIVE(dim, "number of dimensions"); if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); return NULL; } + CHECK_SSIZE_T_RANGE(width, "width"); + if (dim == 2) { + if (height > 0) { + PyErr_SetString(PyExc_ValueError, "height must not be given if dim=2"); + return NULL; + } + } else { + CHECK_SSIZE_T_RANGE(height, "height"); + } + if (igraph_matrix_init(&m, 1, 1)) { igraphmodule_handle_igraph_error(); return NULL; } - if (dim == 2) + if (dim == 2) { ret = igraph_layout_grid(&self->g, &m, width); - else + } else { ret = igraph_layout_grid_3d(&self->g, &m, width, height); + } if (ret != IGRAPH_SUCCESS) { igraphmodule_handle_igraph_error(); @@ -6788,8 +6846,8 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * "miny", "maxy", "minz", "maxz", "dim", NULL }; igraph_matrix_t m; igraph_bool_t use_seed=0; - int ret; - long int niter = 1000, dim = 2; + igraph_error_t ret; + Py_ssize_t niter = 1000, dim = 2; double kkconst, epsilon = 0.0; PyObject *result, *seed_o=Py_None; PyObject *minx_o=Py_None, *maxx_o=Py_None; @@ -6810,7 +6868,7 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * kkconst = igraph_vcount(&self->g); - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lddOOOOOOOl", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nddOOOOOOOn", kwlist, &niter, &epsilon, &kkconst, &seed_o, &minx_o, &maxx_o, @@ -6818,11 +6876,14 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * &minz_o, &maxz_o, &dim)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(dim, "number of dimensions"); if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); return NULL; } + CHECK_SSIZE_T_RANGE_POSITIVE(niter, "number of iterations"); + if (seed_o == 0 || seed_o == Py_None) { if (igraph_matrix_init(&m, 1, 1)) { igraphmodule_handle_igraph_error(); @@ -6874,11 +6935,11 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * } if (dim == 2) ret = igraph_layout_kamada_kawai - (&self->g, &m, use_seed, (igraph_integer_t) niter, epsilon, kkconst, + (&self->g, &m, use_seed, niter, epsilon, kkconst, /*weights=*/ 0, /*bounds*/ minx, maxx, miny, maxy); else ret = igraph_layout_kamada_kawai_3d - (&self->g, &m, use_seed, (igraph_integer_t) niter, epsilon, kkconst, + (&self->g, &m, use_seed, niter, epsilon, kkconst, /*weights=*/ 0, /*bounds*/ minx, maxx, miny, maxy, minz, maxz); DESTROY_VECTORS; @@ -6910,8 +6971,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel "weight_node_edge_dist", NULL }; igraph_matrix_t m; igraph_bool_t use_seed=0; - long int maxiter=10; - long int fineiter=-1; + Py_ssize_t maxiter = 10, fineiter = -1; double cool_fact=0.75; double weight_node_dist=1.0; double weight_border=0.0; @@ -6921,9 +6981,9 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel igraph_real_t density; PyObject *result; PyObject *seed_o=Py_None; - int retval; + igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Olldddddd", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Onndddddd", kwlist, &seed_o, &maxiter, &fineiter, &cool_fact, &weight_node_dist, &weight_border, &weight_edge_lengths, &weight_edge_crossings, @@ -6974,7 +7034,7 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel } retval = igraph_layout_davidson_harel(&self->g, &m, use_seed, - (igraph_integer_t) maxiter, (igraph_integer_t) fineiter, cool_fact, + maxiter, fineiter, cool_fact, weight_node_dist, weight_border, weight_edge_lengths, weight_edge_crossings, weight_node_edge_dist); if (retval) { @@ -7005,13 +7065,14 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, igraph_layout_drl_options_t options; PyObject *result; PyObject *wobj=Py_None, *fixed_o=Py_None, *seed_o=Py_None, *options_o=Py_None; - long dim = 2; - int retval; + Py_ssize_t dim = 2; + igraph_error_t retval; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOl", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOn", kwlist, &wobj, &seed_o, &fixed_o, &options_o, &dim)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(dim, "number of dimensions"); if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); return NULL; @@ -7102,8 +7163,8 @@ PyObject igraph_vector_t *miny=0, *maxy=0; igraph_vector_t *minz=0, *maxz=0; igraph_layout_grid_t grid = IGRAPH_LAYOUT_AUTOGRID; - int ret; - long int niter = 500, dim = 2; + igraph_error_t ret; + Py_ssize_t niter = 500, dim = 2; double start_temp; PyObject *result; PyObject *wobj=Py_None, *seed_o=Py_None; @@ -7124,13 +7185,15 @@ PyObject start_temp = sqrt(igraph_vcount(&self->g)) / 10.0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OldOOOOOOOlO", kwlist, &wobj, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OndOOOOOOOnO", kwlist, &wobj, &niter, &start_temp, &seed_o, &minx_o, &maxx_o, &miny_o, &maxy_o, &minz_o, &maxz_o, &dim, &grid_o)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(niter, "number of iterations"); + if (dim != 2 && dim != 3) { PyErr_SetString(PyExc_ValueError, "number of dimensions must be either 2 or 3"); return NULL; @@ -7198,11 +7261,11 @@ PyObject if (dim == 2) { ret = igraph_layout_fruchterman_reingold - (&self->g, &m, use_seed, (igraph_integer_t) niter, + (&self->g, &m, use_seed, niter, start_temp, grid, weights, minx, maxx, miny, maxy); } else { ret = igraph_layout_fruchterman_reingold_3d - (&self->g, &m, use_seed, (igraph_integer_t) niter, + (&self->g, &m, use_seed, niter, start_temp, weights, minx, maxx, miny, maxy, minz, maxz); } @@ -7234,31 +7297,32 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, { "niter", "node_charge", "node_mass", "spring_length", "spring_constant", "max_sa_movement", "seed", NULL }; igraph_matrix_t m; - long int niter = 500; + Py_ssize_t niter = 500; double node_charge = 0.001, node_mass = 30; - long spring_length = 0; - double spring_constant = 1, max_sa_movement = 5; + double spring_constant = 1, max_sa_movement = 5, spring_length = 0; PyObject *result, *seed_o = Py_None; igraph_bool_t use_seed=0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lddlddO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ndddddO", kwlist, &niter, &node_charge, &node_mass, &spring_length, &spring_constant, &max_sa_movement, &seed_o)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(niter, "number of iterations"); + if (seed_o == 0 || seed_o == Py_None) { if (igraph_matrix_init(&m, 1, 1)) { igraphmodule_handle_igraph_error(); return NULL; } } else { - use_seed=1; + use_seed = 1; if (igraphmodule_PyList_to_matrix_t(seed_o, &m)) return NULL; } - if (igraph_layout_graphopt(&self->g, &m, (igraph_integer_t) niter, + if (igraph_layout_graphopt(&self->g, &m, niter, node_charge, node_mass, spring_length, spring_constant, max_sa_movement, use_seed)) { igraph_matrix_destroy(&m); @@ -7284,7 +7348,7 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, NULL }; igraph_matrix_t m; PyObject *result, *root_o = Py_None; - long int maxiter = 150; + Py_ssize_t maxiter = 150; igraph_integer_t proot = -1; double maxdelta, area, coolexp, repulserad, cellsize; @@ -7294,11 +7358,13 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, repulserad = -1; cellsize = -1; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ldddddO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ndddddO", kwlist, &maxiter, &maxdelta, &area, &coolexp, &repulserad, &cellsize, &root_o)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "maximum number of iterations"); + if (area <= 0) area = igraph_vcount(&self->g)*igraph_vcount(&self->g); if (repulserad <= 0) @@ -7314,7 +7380,7 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, return NULL; } - if (igraph_layout_lgl(&self->g, &m, (igraph_integer_t) maxiter, maxdelta, + if (igraph_layout_lgl(&self->g, &m, maxiter, maxdelta, area, coolexp, repulserad, cellsize, proot)) { igraph_matrix_destroy(&m); igraphmodule_handle_igraph_error(); @@ -7338,7 +7404,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, { "dist", "dim", "arpack_options", NULL }; igraph_matrix_t m; igraph_matrix_t *dist = 0; - long int dim = 2; + Py_ssiez_t dim = 2; PyObject *dist_o = Py_None; PyObject *arpack_options_o = igraphmodule_arpack_options_default; PyObject *result; @@ -7346,11 +7412,13 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, /* arpack_options_o is now unused but we kept here for sake of backwards * compatibility */ - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlO!", kwlist, &dist_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OnO!", kwlist, &dist_o, &dim, &igraphmodule_ARPACKOptionsType, &arpack_options_o)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(dim, "number of dimensions"); + if (dist_o != Py_None) { dist = (igraph_matrix_t*)malloc(sizeof(igraph_matrix_t)); if (!dist) { @@ -7522,16 +7590,18 @@ PyObject *igraphmodule_Graph_layout_sugiyama( igraph_vector_t *weights = 0; igraph_vector_int_t *layers = 0; double hgap = 1, vgap = 1; - long int maxiter = 100; + Py_ssize_t maxiter = 100; PyObject *layers_o = Py_None, *weights_o = Py_None, *extd_to_orig_eids_o = Py_None; PyObject *return_extended_graph = Py_False; PyObject *result; igraphmodule_GraphObject *graph_o; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddlO", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOddnO", kwlist, &layers_o, &weights_o, &hgap, &vgap, &maxiter, &return_extended_graph)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "maximum number of iterations"); + if (igraph_vector_int_init(&extd_to_orig_eids, 0)) { igraphmodule_handle_igraph_error(); return NULL; @@ -7599,14 +7669,16 @@ PyObject *igraphmodule_Graph_layout_bipartite( igraph_matrix_t m; igraph_vector_bool_t *types = 0; double hgap = 1, vgap = 1; - long int maxiter = 100; + Py_ssize_t maxiter = 100; PyObject *types_o = Py_None; PyObject *result; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oddl", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oddn", kwlist, &types_o, &hgap, &vgap, &maxiter)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(maxiter, "maximum number of iterations"); + if (igraph_matrix_init(&m, 1, 1)) { igraphmodule_handle_igraph_error(); return NULL; @@ -8215,20 +8287,22 @@ PyObject *igraphmodule_Graph_Read_GraphML(PyTypeObject * type, { igraphmodule_GraphObject *self; PyObject *fname = NULL; - long int index = 0; + Py_ssize_t index = 0; igraph_t g; igraphmodule_filehandle_t fobj; static char *kwlist[] = { "f", "index", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|l", kwlist, &fname, &index)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|n", kwlist, &fname, &index)) return NULL; - if (igraphmodule_filehandle_init(&fobj, fname, "r")) + CHECK_SSIZE_T_RANGE(index, "graph index"); + + if (igraphmodule_filehandle_init(&fobj, fname, "r")) { return NULL; + } - if (igraph_read_graph_graphml(&g, igraphmodule_filehandle_get(&fobj), - (igraph_integer_t) index)) { + if (igraph_read_graph_graphml(&g, igraphmodule_filehandle_get(&fobj), index)) { igraphmodule_handle_igraph_error(); igraphmodule_filehandle_destroy(&fobj); return NULL; @@ -8248,8 +8322,8 @@ PyObject *igraphmodule_Graph_Read_GraphML(PyTypeObject * type, PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - long source = 0, target = 0; - PyObject *capacity_obj = Py_None, *fname = NULL; + PyObject *capacity_obj = Py_None, *fname = NULL, *source_o, *target_o; + igraph_integer_t source, target; igraphmodule_filehandle_t fobj; igraph_vector_t* capacity = 0; @@ -8257,12 +8331,21 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, "f", "source", "target", "capacity", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oll|O", kwlist, &fname, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO|O", kwlist, &fname, &source, &target, &capacity_obj)) return NULL; - if (igraphmodule_filehandle_init(&fobj, fname, "w")) + if (igraphmodule_PyObject_to_vid(source_o, &source, &self->g)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vid(target_o, &target, &self->g)) { + return NULL; + } + + if (igraphmodule_filehandle_init(&fobj, fname, "w")) { return NULL; + } if (capacity_obj == Py_None) { capacity_obj = PyUnicode_FromString("capacity"); @@ -8594,7 +8677,7 @@ PyObject *igraphmodule_Graph_canonical_permutation( PyObject *list; igraph_bliss_sh_t sh = IGRAPH_BLISS_FL; igraph_vector_int_t labeling, *color = 0; - int retval; + igraph_error_t retval; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &sh_o, &color_o)) return NULL; @@ -8719,7 +8802,7 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, igraph_vector_int_t mapping_12, mapping_21, *map12=0, *map21=0; igraph_bliss_sh_t sh1=IGRAPH_BLISS_FL, sh2=IGRAPH_BLISS_FL; igraph_vector_int_t *color1=0, *color2=0; - int retval; + igraph_error_t retval; static char *kwlist[] = { "other", "return_mapping_12", "return_mapping_21", "sh1", "sh2", "color1", "color2", NULL }; @@ -8913,7 +8996,7 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, igraph_vector_int_t *color1=0, *color2=0; igraph_vector_int_t *edge_color1=0, *edge_color2=0; igraphmodule_i_Graph_isomorphic_vf2_callback_data_t callback_data; - int retval; + igraph_error_t retval; static char *kwlist[] = { "other", "color1", "color2", "edge_color1", "edge_color2", @@ -9252,7 +9335,7 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, igraph_vector_int_t *color1=0, *color2=0; igraph_vector_int_t *edge_color1=0, *edge_color2=0; igraphmodule_i_Graph_isomorphic_vf2_callback_data_t callback_data; - int retval; + igraph_error_t retval; static char *kwlist[] = { "other", "color1", "color2", "edge_color1", "edge_color2", "return_mapping_12", "return_mapping_21", @@ -9935,33 +10018,42 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "vid", "mode", NULL }; - long vid; - PyObject *l1, *l2, *l3, *result, *mode_o=Py_None; + PyObject *l1, *l2, *l3, *result, *mode_o = Py_None, *vid_o; + igraph_integer_t vid; igraph_neimode_t mode = IGRAPH_OUT; igraph_vector_int_t vids; igraph_vector_int_t layers; igraph_vector_int_t parents; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|O", kwlist, &vid, &mode_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &vid_o, &mode_o)) return NULL; + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) return NULL; - if (igraph_vector_int_init(&vids, igraph_vcount(&self->g))) + if (igraphmodule_PyObject_to_vid(vid_o, &vid, &self->g)) { + return NULL; + } + + if (igraph_vector_int_init(&vids, igraph_vcount(&self->g))) { return igraphmodule_handle_igraph_error(); + } + if (igraph_vector_int_init(&layers, igraph_vcount(&self->g))) { igraph_vector_int_destroy(&vids); return igraphmodule_handle_igraph_error(); } + if (igraph_vector_int_init(&parents, igraph_vcount(&self->g))) { igraph_vector_int_destroy(&vids); igraph_vector_int_destroy(&parents); return igraphmodule_handle_igraph_error(); } - if (igraph_bfs_simple - (&self->g, (igraph_integer_t) vid, mode, &vids, &layers, &parents)) { + + if (igraph_bfs_simple(&self->g, vid, mode, &vids, &layers, &parents)) { igraphmodule_handle_igraph_error(); return NULL; } + l1 = igraphmodule_vector_int_t_to_PyList(&vids); l2 = igraphmodule_vector_int_t_to_PyList(&layers); l3 = igraphmodule_vector_int_t_to_PyList(&parents); @@ -9973,9 +10065,11 @@ PyObject *igraphmodule_Graph_bfs(igraphmodule_GraphObject * self, if (l3) { Py_DECREF(l3); } result = NULL; } + igraph_vector_int_destroy(&vids); igraph_vector_int_destroy(&layers); igraph_vector_int_destroy(&parents); + return result; } @@ -10086,20 +10180,24 @@ PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { static char *kwlist[] = { "vid", "mode", NULL }; - PyObject *list = Py_None; - PyObject *mode_o = Py_None; - long int root = -1; + PyObject *list = Py_None, *mode_o = Py_None, *root_o; + igraph_integer_t root; igraph_vector_int_t dom; igraph_neimode_t mode = IGRAPH_OUT; - int res ; + igraph_error_t res; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|O", kwlist, &root, &mode_o)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &root_o, &mode_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_vid(root_o, &root, &self->g)) { return NULL; } if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { return NULL; } + if (mode == IGRAPH_ALL) { mode = IGRAPH_OUT; } @@ -10107,15 +10205,19 @@ PyObject *igraphmodule_Graph_dominator(igraphmodule_GraphObject * self, if (igraph_vector_int_init(&dom, 0)) { return NULL; } + res = igraph_dominator_tree(&self->g, root, &dom, NULL, NULL, mode); - if(res) { + + if (res) { igraph_vector_int_destroy(&dom); return NULL; } + /* The igraph API uses -2 for vertices that are not reachable from the root, * but the Python API seems to be using nan judging from the unit tests */ list = igraphmodule_vector_int_t_to_PyList_with_nan(&dom, -2); igraph_vector_int_destroy(&dom); + return list; } @@ -10459,7 +10561,7 @@ PyObject *igraphmodule_Graph_mincut(igraphmodule_GraphObject * self, static char *kwlist[] = { "source", "target", "capacity", NULL }; PyObject *capacity_object = Py_None, *cut_o, *part_o, *part2_o, *result; PyObject *source_o = Py_None, *target_o = Py_None; - int retval; + igraph_error_t retval; igraph_vector_t capacity_vector; igraph_real_t value; igraph_vector_int_t partition, partition2, cut; @@ -10890,21 +10992,23 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, { static char *kwlist[] = { "min", "max", NULL }; PyObject *list, *item; - long int min_size = 0, max_size = 0; + Py_ssize_t min_size = 0, max_size = 0; igraph_integer_t i, j, n; igraph_vector_ptr_t result; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ll", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nn", kwlist, &min_size, &max_size)) return NULL; + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + if (igraph_vector_ptr_init(&result, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_cliques(&self->g, &result, (igraph_integer_t) min_size, - (igraph_integer_t) max_size)) { + if (igraph_cliques(&self->g, &result, min_size, max_size)) { igraph_vector_ptr_destroy(&result); return igraphmodule_handle_igraph_error(); } @@ -11120,21 +11224,23 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject { static char *kwlist[] = { "min", "max", NULL }; PyObject *list, *item; - long int min_size = 0, max_size = 0; + Py_ssize_t min_size = 0, max_size = 0; igraph_integer_t i, j, n; igraph_vector_ptr_t result; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ll", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nn", kwlist, &min_size, &max_size)) return NULL; + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + if (igraph_vector_ptr_init(&result, 0)) { PyErr_SetString(PyExc_MemoryError, ""); return NULL; } - if (igraph_independent_vertex_sets(&self->g, &result, - (igraph_integer_t) min_size, (igraph_integer_t) max_size)) { + if (igraph_independent_vertex_sets(&self->g, &result, min_size, max_size)) { igraph_vector_ptr_destroy(&result); return igraphmodule_handle_igraph_error(); } @@ -11439,7 +11545,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject */ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "n", "weights", "arpack_options", NULL }; - long int n=-1; + Py_ssizeE_t n = -1; PyObject *cl, *res, *merges, *weights_obj = Py_None; igraph_vector_int_t membership; igraph_vector_t *weights = 0; @@ -11448,13 +11554,21 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj igraphmodule_ARPACKOptionsObject *arpack_options; PyObject *arpack_options_o = igraphmodule_arpack_options_default; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOO!", kwlist, &n, &weights_obj, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOO!", kwlist, &n, &weights_obj, &igraphmodule_ARPACKOptionsType, &arpack_options_o)) { return NULL; } - if (igraph_vector_int_init(&membership, 0)) + if (n < 0) { + n = igraph_vcount(&self->g); + } else { + CHECK_SSIZE_T_RANGE(n, "number of communities"); + n -= 1; + } + + if (igraph_vector_int_init(&membership, 0)) { return igraphmodule_handle_igraph_error(); + } if (igraph_matrix_int_init(&m, 0, 0)) { igraphmodule_handle_igraph_error(); @@ -11462,20 +11576,14 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj return 0; } - if (n<0) - n = igraph_vcount(&self->g); - else - n -= 1; - - if (igraphmodule_attrib_to_vector_t(weights_obj, self, &weights, - ATTRIBUTE_TYPE_EDGE)) { + if (igraphmodule_attrib_to_vector_t(weights_obj, self, &weights, ATTRIBUTE_TYPE_EDGE)) { igraph_matrix_int_destroy(&m); igraph_vector_int_destroy(&membership); return NULL; } arpack_options = (igraphmodule_ARPACKOptionsObject*)arpack_options_o; - if (igraph_community_leading_eigenvector(&self->g, weights, &m, &membership, (igraph_integer_t) n, + if (igraph_community_leading_eigenvector(&self->g, weights, &m, &membership, n, igraphmodule_ARPACKOptions_get(arpack_options), &q, 0, 0, 0, 0, 0, 0)){ igraph_matrix_int_destroy(&m); igraph_vector_int_destroy(&membership); @@ -11566,7 +11674,7 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, { static char *kwlist[] = { "edge_weights", "vertex_weights", "trials", NULL }; PyObject *e_weights = Py_None, *v_weights = Py_None; - unsigned int nb_trials = 10; + Py_ssize_t nb_trials = 10; igraph_vector_t *e_ws = 0, *v_ws = 0; igraph_vector_int_t membership; @@ -11574,11 +11682,13 @@ PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject * self, igraph_real_t codelength; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOI", kwlist, &e_weights, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOn", kwlist, &e_weights, &v_weights, &nb_trials)) { return NULL; } + CHECK_SSIZE_T_RANGE_POSITIVE(nb_trials, "number of trials"); + if (igraph_vector_int_init(&membership, igraph_vcount(&self->g))) { igraphmodule_handle_igraph_error(); return NULL; @@ -11814,7 +11924,7 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, PyObject *impl_o = Py_None; PyObject *res; - long int spins = 25; + Py_ssize_t spins = 25; double start_temp = 1.0; double stop_temp = 0.01; double cool_fact = 0.99; @@ -11825,11 +11935,13 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, igraph_vector_t *weights = 0; igraph_vector_int_t membership; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlOdddOdOd", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OnOdddOdOd", kwlist, &weights_o, &spins, &parupdate_o, &start_temp, &stop_temp, &cool_fact, &update_rule_o, &gamma, &impl_o, &lambda)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(spins, "number of spins"); + if (igraphmodule_PyObject_to_spincomm_update_t(update_rule_o, &update_rule)) { return NULL; } @@ -11850,7 +11962,7 @@ PyObject *igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject *self, } if (igraph_community_spinglass(&self->g, weights, - 0, 0, &membership, 0, (igraph_integer_t) spins, + 0, 0, &membership, 0, spins, PyObject_IsTrue(parupdate_o), start_temp, stop_temp, cool_fact, update_rule, gamma, impl, lambda)) { @@ -11882,15 +11994,17 @@ PyObject *igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject * self, static char *kwlist[] = { "weights", "steps", NULL }; PyObject *ms, *qs, *res, *weights = Py_None; igraph_matrix_int_t merges; - int steps=4; + Py_ssize_t steps = 4; igraph_vector_t q, *ws=0; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", kwlist, &weights, - &steps)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|On", kwlist, &weights, &steps)) return NULL; - if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) + CHECK_SSIZE_T_RANGE_POSITIVE(steps, "number of steps"); + + if (igraphmodule_attrib_to_vector_t(weights, self, &ws, ATTRIBUTE_TYPE_EDGE)) { return NULL; + } igraph_matrix_int_init(&merges, 0, 0); igraph_vector_init(&q, 0); @@ -11939,23 +12053,25 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, PyObject *edge_weights_o = Py_None; PyObject *node_weights_o = Py_None; PyObject *initial_membership_o = Py_None; + PyObject *normalize_resolution = Py_False; PyObject *res = Py_None; int error = 0, i; - long int n_iterations = 2; + Py_ssize_t n_iterations = 2; double resolution_parameter = 1.0; double beta = 0.01; igraph_vector_t *edge_weights = NULL, *node_weights = NULL; igraph_vector_int_t *membership = NULL; igraph_bool_t start = 1; - igraph_bool_t normalize_resolution = 0; igraph_integer_t nb_clusters = 0; igraph_real_t quality = 0.0, prev_quality = -IGRAPH_INFINITY; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdidOl", kwlist, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOdOn", kwlist, &edge_weights_o, &node_weights_o, &resolution_parameter, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) return NULL; + CHECK_SSIZE_T_RANGE_POSITIVE(n_iterations, "number of iterations"); + /* Get edge weights */ if (igraphmodule_attrib_to_vector_t(edge_weights_o, self, &edge_weights, ATTRIBUTE_TYPE_EDGE)) { @@ -11988,7 +12104,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, } } - if (normalize_resolution) + if (PyObject_IsTrue(normalize_resolution)) { /* If we need to normalize the resolution parameter, * we will need to have node weights. */ @@ -12051,10 +12167,7 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, free(membership); } - if (!error) - { return res; } - else - { return NULL; } + return error ? NULL : res; } /********************************************************************** @@ -12069,26 +12182,32 @@ PyObject *igraphmodule_Graph_random_walk(igraphmodule_GraphObject * self, static char *kwlist[] = { "start", "steps", "mode", "stuck", NULL }; PyObject *start_o, *mode_o = Py_None, *stuck_o = Py_None, *res; igraph_integer_t start; - int steps=10; + Py_ssize_t steps = 10; igraph_neimode_t mode = IGRAPH_OUT; igraph_random_walk_stuck_t stuck = IGRAPH_RANDOM_WALK_STUCK_RETURN; igraph_vector_int_t walk; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OiOO", kwlist, &start_o, + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OnOO", kwlist, &start_o, &steps, &mode_o, &stuck_o)) return NULL; - if (igraphmodule_PyObject_to_vid(start_o, &start, &self->g)) + CHECK_SSIZE_T_RANGE(steps, "number of steps"); + + if (igraphmodule_PyObject_to_vid(start_o, &start, &self->g)) { return NULL; + } - if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) + if (igraphmodule_PyObject_to_neimode_t(mode_o, &mode)) { return NULL; + } - if (igraphmodule_PyObject_to_random_walk_stuck_t(stuck_o, &stuck)) + if (igraphmodule_PyObject_to_random_walk_stuck_t(stuck_o, &stuck)) { return NULL; + } - if (igraph_vector_int_init(&walk, steps)) + if (igraph_vector_int_init(&walk, steps)) { return igraphmodule_handle_igraph_error(); + } if (igraph_random_walk(&self->g, &walk, start, mode, steps, stuck)) { igraph_vector_int_destroy(&walk); From c7a17adaaffb23829993b96c87a4a32d4493ddfd Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 00:47:22 +0200 Subject: [PATCH 0390/1681] fix: fix typos and failing tests introduced by the previous commit --- src/_igraph/graphobject.c | 42 +++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 58eab13b2..82057dece 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -7404,7 +7404,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, { "dist", "dim", "arpack_options", NULL }; igraph_matrix_t m; igraph_matrix_t *dist = 0; - Py_ssiez_t dim = 2; + Py_ssize_t dim = 2; PyObject *dist_o = Py_None; PyObject *arpack_options_o = igraphmodule_arpack_options_default; PyObject *result; @@ -8332,7 +8332,7 @@ PyObject *igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject * self, }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO|O", kwlist, &fname, - &source, &target, &capacity_obj)) + &source_o, &target_o, &capacity_obj)) return NULL; if (igraphmodule_PyObject_to_vid(source_o, &source, &self->g)) { @@ -11000,8 +11000,17 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, &min_size, &max_size)) return NULL; - CHECK_SSIZE_T_RANGE(min_size, "minimum size"); - CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; + } + + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; + } if (igraph_vector_ptr_init(&result, 0)) { PyErr_SetString(PyExc_MemoryError, ""); @@ -11149,8 +11158,8 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nnO", kwlist, &min, &max, &file)) return NULL; - CHECK_SSIZE_T_RANGE(min, "minimum clique size"); - CHECK_SSIZE_T_RANGE(max, "maximum clique size"); + CHECK_SSIZE_T_RANGE(min, "minimum size"); + CHECK_SSIZE_T_RANGE(max, "maximum size"); if (file == Py_None) { if (igraph_vector_ptr_init(&result, 0)) { @@ -11232,8 +11241,17 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject &min_size, &max_size)) return NULL; - CHECK_SSIZE_T_RANGE(min_size, "minimum size"); - CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + if (min_size >= 0) { + CHECK_SSIZE_T_RANGE(min_size, "minimum size"); + } else { + min_size = -1; + } + + if (max_size >= 0) { + CHECK_SSIZE_T_RANGE(max_size, "maximum size"); + } else { + max_size = -1; + } if (igraph_vector_ptr_init(&result, 0)) { PyErr_SetString(PyExc_MemoryError, ""); @@ -11545,7 +11563,7 @@ PyObject *igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject */ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "n", "weights", "arpack_options", NULL }; - Py_ssizeE_t n = -1; + Py_ssize_t n = -1; PyObject *cl, *res, *merges, *weights_obj = Py_None; igraph_vector_int_t membership; igraph_vector_t *weights = 0; @@ -12070,7 +12088,11 @@ PyObject *igraphmodule_Graph_community_leiden(igraphmodule_GraphObject *self, &edge_weights_o, &node_weights_o, &resolution_parameter, &normalize_resolution, &beta, &initial_membership_o, &n_iterations)) return NULL; - CHECK_SSIZE_T_RANGE_POSITIVE(n_iterations, "number of iterations"); + if (n_iterations >= 0) { + CHECK_SSIZE_T_RANGE(n_iterations, "number of iterations"); + } else { + n_iterations = -1; + } /* Get edge weights */ if (igraphmodule_attrib_to_vector_t(edge_weights_o, self, &edge_weights, From 16d4737fe6e583d3b863141c5bacc042e523e245 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 00:55:06 +0200 Subject: [PATCH 0391/1681] refactor: removed last remaining instances of "long int"; Python interface should be fully 64-bit compatible now --- src/_igraph/convert.c | 67 +++++++++++++++++++++++--------------- src/_igraph/graphobject.c | 6 ++-- src/_igraph/vertexobject.c | 14 ++++---- 3 files changed, 51 insertions(+), 36 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index ca14f4e24..715afc7eb 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -1741,14 +1741,19 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, igraph_vector_t *result; *vptr = 0; - if (attr_type != ATTRIBUTE_TYPE_EDGE && attr_type != ATTRIBUTE_TYPE_VERTEX) + if (attr_type != ATTRIBUTE_TYPE_EDGE && attr_type != ATTRIBUTE_TYPE_VERTEX) { return 1; - if (o == Py_None) return 0; + } + + if (o == Py_None) { + return 0; + } + if (PyUnicode_Check(o)) { /* Check whether the attribute exists and is numeric */ igraph_attribute_type_t at; igraph_attribute_elemtype_t et; - long int n; + igraph_integer_t n; char *name = PyUnicode_CopyAsString(o); if (attr_type == ATTRIBUTE_TYPE_VERTEX) { @@ -1772,7 +1777,7 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, /* Now that the attribute type has been checked, allocate the target * vector */ result = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); - if (result==0) { + if (result == 0) { PyErr_NoMemory(); free(name); return 1; @@ -1801,7 +1806,7 @@ int igraphmodule_attrib_to_vector_t(PyObject *o, igraphmodule_GraphObject *self, *vptr = result; } else if (PySequence_Check(o)) { result = (igraph_vector_t*)calloc(1, sizeof(igraph_vector_t)); - if (result==0) { + if (result == 0) { PyErr_NoMemory(); return 1; } @@ -1942,7 +1947,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * return 0; if (PyUnicode_Check(o)) { - long int i, n; + igraph_integer_t i, n; /* First, check if the attribute is a "real" boolean */ igraph_attribute_type_t at; @@ -1967,7 +1972,7 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * /* The attribute is a real Boolean attribute. Allocate the target * vector */ result = (igraph_vector_bool_t*)calloc(1, sizeof(igraph_vector_bool_t)); - if (result==0) { + if (result == 0) { PyErr_NoMemory(); free(name); return 1; @@ -2009,12 +2014,12 @@ int igraphmodule_attrib_to_vector_bool_t(PyObject *o, igraphmodule_GraphObject * n = igraph_vector_size(dummy); result = (igraph_vector_bool_t*)calloc(1, sizeof(igraph_vector_bool_t)); igraph_vector_bool_init(result, n); - if (result==0) { + if (result == 0) { igraph_vector_destroy(dummy); free(dummy); PyErr_NoMemory(); return 1; } - for (i=0; ig); - else if (type == ATTRHASH_IDX_EDGE) n=igraph_ecount(&g->g); - else n=1; + if (type == ATTRHASH_IDX_VERTEX) { + n = igraph_vcount(&g->g); + } else if (type == ATTRHASH_IDX_EDGE) { + n = igraph_ecount(&g->g); + } else { + n = 1; + } + + if (igraph_vector_init(v, n)) { + return 1; + } - if (igraph_vector_init(v, n)) return 1; - for (i=0; igref; PyObject *names, *dict; - long i, n; + Py_ssize_t i, n; if (!igraphmodule_Vertex_Validate((PyObject*)self)) return 0; - dict=PyDict_New(); - if (!dict) return NULL; + dict = PyDict_New(); + if (!dict) { + return NULL; + } - names=igraphmodule_Graph_vertex_attributes(o); + names = igraphmodule_Graph_vertex_attributes(o); if (!names) { Py_DECREF(dict); return NULL; } - n=PyList_Size(names); - for (i=0; i Date: Fri, 10 Sep 2021 11:40:13 +0200 Subject: [PATCH 0392/1681] chore: setup.py cleanup + added typing to setup.py --- setup.py | 248 +++++++++++++++++++++++++++---------------------------- tox.ini | 5 +- 2 files changed, 126 insertions(+), 127 deletions(-) diff --git a/setup.py b/setup.py index 3ed791065..4d2a7ffcf 100644 --- a/setup.py +++ b/setup.py @@ -11,12 +11,6 @@ print("This module requires Python >= 3.6") sys.exit(0) -# Check whether we are compiling for PyPy. Headers will not be installed -# for PyPy. -SKIP_HEADER_INSTALL = (platform.python_implementation() == "PyPy") or ( - "SKIP_HEADER_INSTALL" in os.environ -) - ########################################################################### from setuptools import setup, Command, Extension @@ -28,10 +22,12 @@ import sys import sysconfig +from contextlib import contextmanager from pathlib import Path from select import select from shutil import which from time import sleep +from typing import List, Iterable, Iterator, Optional, Tuple, TypeVar, Union ########################################################################### @@ -39,25 +35,31 @@ LIBIGRAPH_FALLBACK_LIBRARIES = ["igraph"] LIBIGRAPH_FALLBACK_LIBRARY_DIRS = [] +# Check whether we are compiling for PyPy. Headers will not be installed +# for PyPy. +SKIP_HEADER_INSTALL = (platform.python_implementation() == "PyPy") or ( + "SKIP_HEADER_INSTALL" in os.environ +) + ########################################################################### -is_windows = platform.system() == "windows" +T = TypeVar("T") -def building_on_windows_msvc(): +def building_on_windows_msvc() -> bool: """Returns True when using the non-MinGW CPython interpreter on Windows""" return platform.system() == "Windows" and sysconfig.get_platform() != "mingw" -def exclude_from_list(items, items_to_exclude): +def exclude_from_list(items: Iterable[T], items_to_exclude: Iterable[T]) -> List[T]: """Excludes certain items from a list, keeping the original order of the remaining items.""" itemset = set(items_to_exclude) return [item for item in items if item not in itemset] -def find_static_library(library_name, library_path): +def find_static_library(library_name: str, library_path: List[str]) -> Optional[str]: """Given the raw name of a library in `library_name`, tries to find a static library with this name in the given `library_path`. `library_path` is automatically extended with common library directories on Linux and Mac @@ -88,15 +90,17 @@ def find_static_library(library_name, library_path): return full_path -def first(iterable): +def first(iterable: Iterable[T]) -> T: """Returns the first element from the given iterable.""" for item in iterable: return item raise ValueError("iterable is empty") -def get_output(args, encoding="utf-8"): - """Returns the output of a command returning a single line of output.""" +def get_output(args, encoding: str = "utf-8") -> Tuple[str, int]: + """Returns the output of a command returning a single line of output, and + the exit code of the command. + """ PIPE = subprocess.PIPE try: p = subprocess.Popen(args, shell=False, stdin=PIPE, stdout=PIPE, stderr=PIPE) @@ -105,25 +109,23 @@ def get_output(args, encoding="utf-8"): except OSError: stdout, stderr = None, None returncode = 77 - if encoding and type(stdout).__name__ == "bytes": + if isinstance(stdout, bytes): stdout = str(stdout, encoding=encoding) - if encoding and type(stderr).__name__ == "bytes": + if isinstance(stderr, bytes): stderr = str(stderr, encoding=encoding) - return stdout, returncode + return (stdout or ""), returncode -def get_output_single_line(args, encoding="utf-8"): - """Returns the output of a command returning a single line of output, - stripped from any trailing newlines.""" +def get_output_single_line(args, encoding: str = "utf-8") -> Tuple[str, int]: + """Returns the first line of the output of a command, stripped from any + trailing newlines, and the exit code of the command. + """ stdout, returncode = get_output(args, encoding=encoding) - if stdout is not None: - line, _, _ = stdout.partition("\n") - else: - line = None + line, _, _ = stdout.partition("\n") return line, returncode -def is_unix_like(platform=None): +def is_unix_like(platform: str = sys.platform) -> bool: """Returns whether the given platform is a Unix-like platform with the usual Unix filesystem. When the parameter is omitted, it defaults to ``sys.platform`` """ @@ -136,12 +138,10 @@ def is_unix_like(platform=None): ) -def wait_for_keypress(seconds): +def wait_for_keypress(seconds: float) -> None: """Wait for a keypress or until the given number of seconds have passed, whichever happens first. """ - global is_windows - while seconds > 0: if seconds > 1: plural = "s" @@ -154,10 +154,10 @@ def wait_for_keypress(seconds): ) sys.stdout.flush() - if is_windows: - from msvcrt import kbhit + if platform.system() == "Windows": + from msvcrt import kbhit # type: ignore - for i in range(10): + for _ in range(10): if kbhit(): seconds = 0 break @@ -174,62 +174,48 @@ def wait_for_keypress(seconds): sys.stdout.write("\r" + " " * 65 + "\r") -########################################################################### - - -class IgraphCCoreBuilder: - """Superclass for classes responsible for downloading and building the - C core of igraph if it is not installed yet. - """ - - def create_build_config_file(self, install_folder, libraries): - with (install_folder / "build.cfg").open("w") as fp: - fp.write(repr(libraries)) - - def parse_pkgconfig_file(self, filename): - building_on_windows = building_on_windows_msvc() - - if building_on_windows: - libraries = ["igraph"] - else: - libraries = [] - with filename.open("r") as fp: - for line in fp: - if line.startswith("Libs: ") or line.startswith("Libs.private: "): - words = line.strip().split() - libraries.extend( - word[2:] for word in words if word.startswith("-l") - ) - - if not libraries: - # Educated guess - libraries = ["igraph"] - - return libraries +@contextmanager +def working_directory(dir: Union[str, Path]) -> Iterator[None]: + cwd = os.getcwd() + os.chdir(dir) + try: + yield + finally: + os.chdir(cwd) ########################################################################### -class IgraphCCoreCMakeBuilder(IgraphCCoreBuilder): +class IgraphCCoreCMakeBuilder: """Class responsible for downloading and building the C core of igraph if it is not installed yet, assuming that the C core uses CMake as the build tool. This is the case from igraph 0.9. - - Returns: - False if the build failed or the list of libraries to link to when - linking the Python interface to igraph """ - def compile_in(self, source_folder, build_folder, install_folder): + def compile_in( + self, source_folder: Path, build_folder: Path, install_folder: Path + ) -> Union[bool, List[str]]: """Compiles igraph from its source code in the given folder. - source_folder is the name of the folder that contains igraph's source - files. build_folder is the name of the folder where the build should - be executed. Both must be absolute paths. + Parameters: + source_folder: absolute path to the folder that contains igraph's + source files + build_folder: absolute path to the folder where the build should be + executed + install_folder: absolute path to the folder where the built library + should be installed + + Returns: + False if the build failed or the list of libraries to link to when + linking the Python interface to igraph """ - global is_windows + with working_directory(build_folder): + return self._compile_in(source_folder, build_folder, install_folder) + def _compile_in( + self, source_folder: Path, build_folder: Path, install_folder: Path + ) -> Union[bool, List[str]]: cmake = which("cmake") if not cmake: print( @@ -239,7 +225,6 @@ def compile_in(self, source_folder, build_folder, install_folder): return False build_to_source_folder = os.path.relpath(source_folder, build_folder) - os.chdir(build_folder) print("Configuring build...") args = [cmake] @@ -290,12 +275,39 @@ def compile_in(self, source_folder, build_folder, install_folder): ] for candidate in pkgconfig_candidates: if candidate.exists(): - return self.parse_pkgconfig_file(candidate) + return self._parse_pkgconfig_file(candidate) raise RuntimeError( "no igraph.pc was found in the installation folder of igraph" ) + def create_build_config_file( + self, install_folder: Path, libraries: List[str] + ) -> None: + with (install_folder / "build.cfg").open("w") as fp: + fp.write(repr(libraries)) + + def _parse_pkgconfig_file(self, filename: Path) -> List[str]: + building_on_windows = building_on_windows_msvc() + + if building_on_windows: + libraries = ["igraph"] + else: + libraries = [] + with filename.open("r") as fp: + for line in fp: + if line.startswith("Libs: ") or line.startswith("Libs.private: "): + words = line.strip().split() + libraries.extend( + word[2:] for word in words if word.startswith("-l") + ) + + if not libraries: + # Educated guess + libraries = ["igraph"] + + return libraries + ########################################################################### @@ -312,25 +324,26 @@ def __init__(self): self.extra_objects = [] self.static_extension = False self.use_pkgconfig = False + self.c_core_built = False self._has_pkgconfig = None self.excluded_include_dirs = [] self.excluded_library_dirs = [] self.wait = platform.system() != "Windows" @property - def has_pkgconfig(self): + def has_pkgconfig(self) -> bool: """Returns whether ``pkg-config`` is available on the current system and it knows about igraph or not.""" if self._has_pkgconfig is None: if self.use_pkgconfig: - line, exit_code = get_output_single_line(["pkg-config", "igraph"]) + _, exit_code = get_output_single_line(["pkg-config", "igraph"]) self._has_pkgconfig = exit_code == 0 else: self._has_pkgconfig = False return self._has_pkgconfig @property - def build_c_core(self): + def build_c_core(self) -> Command: """Returns a class representing a custom setup.py command that builds the C core of igraph. @@ -359,7 +372,7 @@ def run(self): return build_c_core @property - def build_ext(self): + def build_ext(self) -> Command: """Returns a class that can be used as a replacement for the ``build_ext`` command in ``setuptools`` and that will compile the C core of igraph before compiling the Python extension. @@ -467,60 +480,48 @@ def sdist(self): """ from setuptools.command.sdist import sdist - def is_git_repo(folder): - return os.path.exists(os.path.join(folder, ".git")) + def is_git_repo(folder) -> bool: + return (Path(folder) / ".git").exists() - def cleanup_git_repo(folder): - folder = str(folder) - cwd = os.getcwd() - try: - os.chdir(folder) + def cleanup_git_repo(folder) -> None: + with working_directory(folder): if os.path.exists(".git"): retcode = subprocess.call("git clean -dfx", shell=True) if retcode: raise RuntimeError(f"Failed to clean {folder} with git") - finally: - os.chdir(cwd) class custom_sdist(sdist): def run(self): - igraph_source_repo = os.path.join("vendor", "source", "igraph") - igraph_build_dir = os.path.join("vendor", "build", "igraph") - version_file = os.path.join(igraph_source_repo, "IGRAPH_VERSION") + igraph_source_repo = Path("vendor", "source", "igraph") + igraph_build_dir = Path("vendor", "build", "igraph") + version_file = igraph_source_repo / "IGRAPH_VERSION" version = None # Check whether the source repo contains an IGRAPH_VERSION file, # and extract the version number from that - if os.path.exists(version_file): - with open(version_file, "r") as fp: - version = fp.read().strip().split("\n")[0] + if version_file.exists(): + version = version_file.read_text().strip().split("\n")[0] # If no IGRAPH_VERSION file exists, but we have a git repo, try # git describe if not version and is_git_repo(igraph_source_repo): - cwd = os.getcwd() - try: - os.chdir(igraph_source_repo) + with working_directory(igraph_source_repo): version = ( subprocess.check_output("git describe", shell=True) .decode("utf-8") .strip() ) - finally: - os.chdir(cwd) # If we still don't have a version number, try to parse it from # include/igraph_version.h if not version: - version_header = os.path.join( - igraph_build_dir, "include", "igraph_version.h" - ) - if not os.path.exists(version_header): + version_header = igraph_build_dir / "include" / "igraph_version.h" + if not version_header.exists(): raise RuntimeError( "You need to build the C core of igraph first before generating a source tarball of python-igraph" ) - with open(version_header, "r") as fp: + with version_header.open("r") as fp: lines = [ line.strip() for line in fp @@ -543,20 +544,20 @@ def run(self): cleanup_git_repo(igraph_source_repo) # Copy the generated parser sources from the build folder - parser_dir = os.path.join(igraph_build_dir, "src", "io", "parsers") - if os.path.isdir(parser_dir): + parser_dir = igraph_build_dir / "src" / "io" / "parsers" + if parser_dir.is_dir(): shutil.copytree( parser_dir, - os.path.join(igraph_source_repo, "src", "io", "parsers"), + igraph_source_repo / "src" / "io" / "parsers" ) else: raise RuntimeError( - f"You need to build the C core of igraph first before generating a source tarball of python-igraph" + "You need to build the C core of igraph first before " + "generating a source tarball of python-igraph" ) # Add a version file to the tarball - with open(version_file, "w") as fp: - fp.write(version) + version_file.write_text(version) # Run the original sdist command retval = sdist.run(self) @@ -568,7 +569,7 @@ def run(self): return custom_sdist - def compile_igraph_from_vendor_source(self): + def compile_igraph_from_vendor_source(self) -> bool: """Compiles igraph from the vendored source code inside `vendor/source/igraph`. This folder typically comes from a git submodule. """ @@ -601,27 +602,25 @@ def compile_igraph_from_vendor_source(self): Path(build_folder).mkdir(parents=True, exist_ok=True) - cwd = os.getcwd() - try: - libraries = igraph_builder.compile_in( - source_folder=source_folder, - build_folder=build_folder, - install_folder=install_folder, - ) - finally: - os.chdir(cwd) + libraries = igraph_builder.compile_in( + source_folder=source_folder, + build_folder=build_folder, + install_folder=install_folder, + ) if libraries is False: print("Build failed for the C core of igraph.") print("") sys.exit(1) + assert not isinstance(libraries, bool) + igraph_builder.create_build_config_file(install_folder, libraries) self.use_vendored_igraph() return True - def configure(self, ext): + def configure(self, ext) -> None: """Configures the given Extension object using this build configuration.""" ext.include_dirs = exclude_from_list( self.include_dirs, self.excluded_include_dirs @@ -636,7 +635,7 @@ def configure(self, ext): ext.extra_objects = self.extra_objects ext.define_macros = self.define_macros - def detect_from_pkgconfig(self): + def detect_from_pkgconfig(self) -> bool: """Detects the igraph include directory, library directory and the list of libraries to link to using ``pkg-config``.""" if not buildcfg.has_pkgconfig: @@ -655,7 +654,7 @@ def detect_from_pkgconfig(self): self.include_dirs = [opt[2:] for opt in opts if opt.startswith("-I")] return True - def print_build_info(self): + def print_build_info(self) -> None: """Prints the include and library path being used for debugging purposes.""" if self.static_extension == "only_igraph": build_type = "dynamic extension with vendored igraph source" @@ -719,7 +718,7 @@ def replace_static_libraries(self, only=None, exclusions=None): else: print(f"Warning: could not find static library of {library_name}.") - def use_vendored_igraph(self): + def use_vendored_igraph(self) -> None: """Assumes that igraph is installed already in ``vendor/install/igraph`` and sets up the include and library paths and the library names accordingly.""" building_on_windows = building_on_windows_msvc() @@ -748,7 +747,7 @@ def use_vendored_igraph(self): if buildcfg_file.exists(): buildcfg.libraries = eval(buildcfg_file.open("r").read()) - def use_educated_guess(self): + def use_educated_guess(self) -> None: """Tries to guess the proper library names, include and library paths if everything else failed.""" @@ -790,7 +789,6 @@ def use_educated_guess(self): buildcfg = BuildConfiguration() buildcfg.process_args_from_command_line() - # Define the extension sources = glob.glob(os.path.join("src", "_igraph", "*.c")) sources.append(os.path.join("src", "_igraph", "force_cpp_linker.cpp")) diff --git a/tox.ini b/tox.ini index d144c3963..c51b45b25 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,7 @@ setenv = TESTING_IN_TOX=1 [flake8] -max-line-length = 88 -extend-ignore = E203, W503 +max-line-length = 80 +select = C,E,F,W,B,B950 +ignore = E203,E501,W503 From 93d540f42a2255f3d6a4680e50afbdf5ec277ae2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 11:41:15 +0200 Subject: [PATCH 0393/1681] fix: IGRAPH_USE_INTERNAL_CXSPARSE is not an option any more in igraph's C core --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d2a7ffcf..c7dbb458c 100644 --- a/setup.py +++ b/setup.py @@ -230,7 +230,7 @@ def _compile_in( args = [cmake] # Build the Python interface with vendored libraries - for deps in "ARPACK BLAS CXSPARSE GLPK GMP LAPACK".split(): + for deps in "ARPACK BLAS GLPK GMP LAPACK".split(): args.append("-DIGRAPH_USE_INTERNAL_" + deps + "=ON") # -fPIC is needed on Linux so we can link to a static igraph lib from a From 449ac36e800b5c52a080f5bd150fc49538d4b73b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 11:41:15 +0200 Subject: [PATCH 0394/1681] fix: IGRAPH_USE_INTERNAL_CXSPARSE is not an option any more in igraph's C core --- warnings_converted_to_errors.py | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 warnings_converted_to_errors.py diff --git a/warnings_converted_to_errors.py b/warnings_converted_to_errors.py deleted file mode 100644 index 76ee5230b..000000000 --- a/warnings_converted_to_errors.py +++ /dev/null @@ -1,11 +0,0 @@ -from igraph import Graph -import warnings - -warnings.simplefilter('error', 'Cannot shuffle graph') -degseq = [1,2,2,3] -try: - testgraph = Graph.Degree_Sequence(degseq,method = "vl") -except RuntimeWarning: - print(degseq) -else: - print("go on") From 90536171a36a5e2ab4ca74c69d3cd1adc071b738 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 13:47:18 +0200 Subject: [PATCH 0395/1681] refactor: centralized inclusion of Python.h, started migrating to Python's limited API --- src/_igraph/arpackobject.c | 126 ++++++++++------------------------ src/_igraph/arpackobject.h | 12 ++-- src/_igraph/attributes.c | 1 - src/_igraph/attributes.h | 3 +- src/_igraph/bfsiter.h | 3 +- src/_igraph/common.h | 2 +- src/_igraph/convert.c | 1 - src/_igraph/convert.h | 3 +- src/_igraph/dfsiter.h | 3 +- src/_igraph/edgeobject.h | 3 +- src/_igraph/edgeseqobject.h | 3 +- src/_igraph/error.h | 3 +- src/_igraph/filehandle.h | 3 +- src/_igraph/graphobject.c | 16 ++--- src/_igraph/graphobject.h | 3 +- src/_igraph/igraphmodule.c | 10 +-- src/_igraph/indexing.h | 3 +- src/_igraph/operators.h | 2 +- src/_igraph/preamble.h | 29 ++++++++ src/_igraph/pyhelpers.h | 2 +- src/_igraph/random.h | 2 +- src/_igraph/vertexobject.h | 3 +- src/_igraph/vertexseqobject.c | 1 - src/_igraph/vertexseqobject.h | 3 +- 24 files changed, 112 insertions(+), 128 deletions(-) create mode 100644 src/_igraph/preamble.h diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 7f31f506c..142f0a074 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -24,25 +24,16 @@ #include "graphobject.h" #include "error.h" +PyTypeObject* igraphmodule_ARPACKOptionsType; PyObject* igraphmodule_arpack_options_default; -/** - * \ingroup python_interface_arpack - * \brief Checks if the object is an ARPACK parameter object - */ -int igraphmodule_ARPACKOptions_Check(PyObject *ob) { - if (ob) return PyType_IsSubtype(ob->ob_type, &igraphmodule_ARPACKOptionsType); - return 0; -} - /** * \ingroup python_interface_arpack * \brief Allocates a new ARPACK parameters object */ PyObject* igraphmodule_ARPACKOptions_new() { igraphmodule_ARPACKOptionsObject* self; - self=PyObject_New(igraphmodule_ARPACKOptionsObject, - &igraphmodule_ARPACKOptionsType); + self = PyObject_New(igraphmodule_ARPACKOptionsObject, igraphmodule_ARPACKOptionsType); if (self) { igraph_arpack_options_init(&self->params); igraph_arpack_options_init(&self->params_out); @@ -54,16 +45,16 @@ PyObject* igraphmodule_ARPACKOptions_new() { * \ingroup python_interface_arpack * \brief Deallocates a Python representation of a given ARPACK parameters object */ -void igraphmodule_ARPACKOptions_dealloc( - igraphmodule_ARPACKOptionsObject* self) { - /*igraph_arpack_options_destroy(&self->params);*/ +static void igraphmodule_ARPACKOptions_dealloc(igraphmodule_ARPACKOptionsObject* self) { + PyTypeObject *tp = Py_TYPE(self); PyObject_Del((PyObject*)self); + Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } /** \ingroup python_interface_arpack * \brief Returns one of the attributes of a given ARPACK parameters object */ -PyObject* igraphmodule_ARPACKOptions_getattr( +static PyObject* igraphmodule_ARPACKOptions_getattr( igraphmodule_ARPACKOptionsObject* self, char* attrname) { PyObject *result = NULL; @@ -117,7 +108,7 @@ PyObject* igraphmodule_ARPACKOptions_getattr( /** \ingroup python_interface_arpack * \brief Sets one of the attributes of a given ARPACK parameters object */ -int igraphmodule_ARPACKOptions_setattr( +static int igraphmodule_ARPACKOptions_setattr( igraphmodule_ARPACKOptionsObject* self, char* attrname, PyObject* value) { if (value == 0) { @@ -175,63 +166,12 @@ igraph_arpack_options_t *igraphmodule_ARPACKOptions_get( * * \return the formatted textual representation as a \c PyObject */ -PyObject* igraphmodule_ARPACKOptions_str( - igraphmodule_ARPACKOptionsObject *self) { - PyObject *s; - - s=PyUnicode_FromFormat("ARPACK parameters"); - return s; +PyObject* igraphmodule_ARPACKOptions_str(igraphmodule_ARPACKOptionsObject *self) { + return PyUnicode_FromString("ARPACK parameters"); } -/** - * \ingroup python_interface_arpack - * Method table for the \c igraph.ARPACKOptions object - */ -PyMethodDef igraphmodule_ARPACKOptions_methods[] = { - /*{"attributes", (PyCFunction)igraphmodule_Edge_attributes, - METH_NOARGS, - "attributes()\n--\n\n" - "Returns the attribute list of the graph's edges\n" - },*/ - {NULL} -}; - -/** - * \ingroup python_interface_edge - * Getter/setter table for the \c igraph.ARPACKOptions object - */ -PyGetSetDef igraphmodule_ARPACKOptions_getseters[] = { - /*{"tuple", (getter)igraphmodule_Edge_get_tuple, NULL, - "Source and target node index of this edge as a tuple", NULL - },*/ - {NULL} -}; - -/** \ingroup python_interface_edge - * Python type object referencing the methods Python calls when it performs - * various operations on an ARPACK parameters object - */ -PyTypeObject igraphmodule_ARPACKOptionsType = { - PyVarObject_HEAD_INIT(0, 0) - "igraph.ARPACKOptions", /* tp_name */ - sizeof(igraphmodule_ARPACKOptionsObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)igraphmodule_ARPACKOptions_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - (getattrfunc)igraphmodule_ARPACKOptions_getattr, /* tp_getattr */ - (setattrfunc)igraphmodule_ARPACKOptions_setattr, /* tp_setattr */ - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - 0, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - (reprfunc)igraphmodule_ARPACKOptions_str, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ +PyDoc_STRVAR( + igraphmodule_ARPACKOptions_doc, "Class representing the parameters of the ARPACK module.\n\n" "ARPACK is a Fortran implementation of the implicitly restarted\n" "Arnoldi method, an algorithm for calculating some of the\n" @@ -257,24 +197,30 @@ PyTypeObject igraphmodule_ARPACKOptionsType = { " - C{numop}: total number of OP*x operations\n\n" " - C{numopb}: total number of B*x operations if C{bmat} is C{'G'}\n\n" " - C{numreo}: total number of steps of re-orthogonalization\n\n" - "", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - igraphmodule_ARPACKOptions_methods, /* tp_methods */ - 0, /* tp_members */ - igraphmodule_ARPACKOptions_getseters, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - (newfunc)igraphmodule_ARPACKOptions_new, /* tp_new */ - 0, /* tp_free */ +); + +PyType_Slot igraphmodule_ARPACKOptions_slots[] = { + { Py_tp_new, igraphmodule_ARPACKOptions_new }, + { Py_tp_dealloc, igraphmodule_ARPACKOptions_dealloc }, + { Py_tp_getattr, igraphmodule_ARPACKOptions_getattr }, + { Py_tp_setattr, igraphmodule_ARPACKOptions_setattr }, + { Py_tp_str, igraphmodule_ARPACKOptions_str }, + { Py_tp_doc, (void*) igraphmodule_ARPACKOptions_doc }, + { 0 } }; +/** \ingroup python_interface_arpack + * Python type specification for \c igraph.ARPACKOptions + */ +PyType_Spec igraphmodule_ARPACKOptions_spec = { + "igraph.ARPACKOptions", /* name */ + sizeof(igraphmodule_ARPACKOptionsObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ + igraphmodule_ARPACKOptions_slots, /* slots */ +}; + +int igraphmodule_ARPACKOptions_register_type() { + igraphmodule_ARPACKOptionsType = (PyTypeObject*) PyType_FromSpec(&igraphmodule_ARPACKOptions_spec); + return igraphmodule_ARPACKOptionsType == 0; +} diff --git a/src/_igraph/arpackobject.h b/src/_igraph/arpackobject.h index b9d663d30..5d5fd31d0 100644 --- a/src/_igraph/arpackobject.h +++ b/src/_igraph/arpackobject.h @@ -23,7 +23,9 @@ #ifndef PYTHON_ARPACKOBJECT_H #define PYTHON_ARPACKOBJECT_H -#include +#define Py_LIMITED_API 0x03060000 +#include "preamble.h" + #include #include "graphobject.h" @@ -31,7 +33,6 @@ * \ingroup python_interface * \defgroup python_interface_arpack ARPACK parameters object */ -extern PyTypeObject igraphmodule_ARPACKOptionsType; /** * \ingroup python_interface_arpack @@ -43,13 +44,12 @@ typedef struct { igraph_arpack_options_t params_out; } igraphmodule_ARPACKOptionsObject; +extern PyTypeObject* igraphmodule_ARPACKOptionsType; extern PyObject* igraphmodule_arpack_options_default; -void igraphmodule_ARPACKOptions_dealloc(igraphmodule_ARPACKOptionsObject* self); +int igraphmodule_ARPACKOptions_register_type(); PyObject* igraphmodule_ARPACKOptions_new(void); -PyObject* igraphmodule_ARPACKOptions_str(igraphmodule_ARPACKOptionsObject *self); -#define igraphmodule_ARPACKOptions_CheckExact(ob) ((ob)->ob_type == &igraphmodule_ARPACKOptionsType) igraph_arpack_options_t *igraphmodule_ARPACKOptions_get(igraphmodule_ARPACKOptionsObject *self); -int igraphmodule_ARPACKOptions_Check(PyObject *ob); + #endif diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 7c4eb8749..95375559a 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -20,7 +20,6 @@ */ -#include #include "attributes.h" #include "common.h" #include "convert.h" diff --git a/src/_igraph/attributes.h b/src/_igraph/attributes.h index 5e68f2a2b..c3faa95ac 100644 --- a/src/_igraph/attributes.h +++ b/src/_igraph/attributes.h @@ -23,7 +23,8 @@ #ifndef PY_IGRAPH_ATTRIBUTES_H #define PY_IGRAPH_ATTRIBUTES_H -#include +#include "preamble.h" + #include #include #include diff --git a/src/_igraph/bfsiter.h b/src/_igraph/bfsiter.h index 94ddb6113..4fa665462 100644 --- a/src/_igraph/bfsiter.h +++ b/src/_igraph/bfsiter.h @@ -23,7 +23,8 @@ #ifndef PYTHON_BFSITER_H #define PYTHON_BFSITER_H -#include +#include "preamble.h" + #include "graphobject.h" /** diff --git a/src/_igraph/common.h b/src/_igraph/common.h index e74a6fa67..31a434353 100644 --- a/src/_igraph/common.h +++ b/src/_igraph/common.h @@ -23,7 +23,7 @@ #ifndef PYTHON_COMMON_H #define PYTHON_COMMON_H -#include +#include "preamble.h" #ifdef RC_DEBUG # define RC_ALLOC(T, P) fprintf(stderr, "[ alloc ] " T " @ %p\n", P) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 717158136..8b2330b57 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -22,7 +22,6 @@ /************************ Miscellaneous functions *************************/ -#include #include #include "attributes.h" #include "convert.h" diff --git a/src/_igraph/convert.h b/src/_igraph/convert.h index bff912b83..4c50219a2 100644 --- a/src/_igraph/convert.h +++ b/src/_igraph/convert.h @@ -28,7 +28,8 @@ #ifndef PYTHON_CONVERT_H #define PYTHON_CONVERT_H -#include +#include "preamble.h" + #include #include #include "graphobject.h" diff --git a/src/_igraph/dfsiter.h b/src/_igraph/dfsiter.h index 0a094c592..880965fcd 100644 --- a/src/_igraph/dfsiter.h +++ b/src/_igraph/dfsiter.h @@ -23,7 +23,8 @@ #ifndef PYTHON_DFSITER_H #define PYTHON_DFSITER_H -#include +#include "preamble.h" + #include "graphobject.h" /** diff --git a/src/_igraph/edgeobject.h b/src/_igraph/edgeobject.h index 2d7f3a259..ac0a93668 100644 --- a/src/_igraph/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -23,7 +23,8 @@ #ifndef PYTHON_EDGEOBJECT_H #define PYTHON_EDGEOBJECT_H -#include +#include "preamble.h" + #include "graphobject.h" /** diff --git a/src/_igraph/edgeseqobject.h b/src/_igraph/edgeseqobject.h index 7a410b9e1..b8fd2f127 100644 --- a/src/_igraph/edgeseqobject.h +++ b/src/_igraph/edgeseqobject.h @@ -23,7 +23,8 @@ #ifndef PYTHON_EDGESEQOBJECT_H #define PYTHON_EDGESEQOBJECT_H -#include +#include "preamble.h" + #include "graphobject.h" /** diff --git a/src/_igraph/error.h b/src/_igraph/error.h index 8d54e728c..03c50ab10 100644 --- a/src/_igraph/error.h +++ b/src/_igraph/error.h @@ -23,7 +23,8 @@ #ifndef PYTHON_ERROR_H #define PYTHON_ERROR_H -#include +#include "preamble.h" + #include /** \defgroup python_interface_errors Error handling diff --git a/src/_igraph/filehandle.h b/src/_igraph/filehandle.h index 68bf3c3be..6f3c5acf7 100644 --- a/src/_igraph/filehandle.h +++ b/src/_igraph/filehandle.h @@ -23,7 +23,8 @@ #ifndef PYTHON_FILEHANDLE_H #define PYTHON_FILEHANDLE_H -#include +#include "preamble.h" + #include /** diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 31ffe0caf..2bb48112b 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -3774,7 +3774,7 @@ PyObject *igraphmodule_Graph_authority_score( igraph_vector_t res, *weights = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, - &scale_o, &igraphmodule_ARPACKOptionsType, + &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options_o, &return_eigenvalue)) return NULL; @@ -4634,7 +4634,7 @@ PyObject* igraphmodule_Graph_eigen_adjacency(igraphmodule_GraphObject *self, if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!", kwlist, &algorithm_o, &which_o, - &igraphmodule_ARPACKOptionsType, + igraphmodule_ARPACKOptionsType, &arpack_options)) { return NULL; } @@ -4789,7 +4789,7 @@ PyObject *igraphmodule_Graph_eigenvector_centrality( if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO!O", kwlist, &directed_o, &scale_o, &weights_o, - &igraphmodule_ARPACKOptionsType, + igraphmodule_ARPACKOptionsType, &arpack_options, &return_eigenvalue)) return NULL; @@ -5154,7 +5154,7 @@ PyObject *igraphmodule_Graph_hub_score( igraph_vector_t res, *weights = 0; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO!O", kwlist, &weights_o, - &scale_o, &igraphmodule_ARPACKOptionsType, + &scale_o, igraphmodule_ARPACKOptionsType, &arpack_options, &return_eigenvalue)) return NULL; @@ -5349,8 +5349,8 @@ PyObject *igraphmodule_Graph_personalized_pagerank(igraphmodule_GraphObject *sel if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOdOOOO!Olf", kwlist, &vobj, &directed, &damping, &robj, - &rvsobj, &wobj, - &igraphmodule_ARPACKOptionsType, + &rvsobj, &wobj, + igraphmodule_ARPACKOptionsType, &arpack_options_o, &algo_o, &niter, &eps)) @@ -7339,7 +7339,7 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, * compatibility */ if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OlO!", kwlist, &dist_o, - &dim, &igraphmodule_ARPACKOptionsType, + &dim, igraphmodule_ARPACKOptionsType, &arpack_options_o)) return NULL; @@ -11428,7 +11428,7 @@ PyObject *igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObj PyObject *arpack_options_o = igraphmodule_arpack_options_default; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|lOO!", kwlist, &n, &weights_obj, - &igraphmodule_ARPACKOptionsType, &arpack_options_o)) { + igraphmodule_ARPACKOptionsType, &arpack_options_o)) { return NULL; } diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 8ee6e4fab..949b990bb 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -23,7 +23,8 @@ #ifndef PYTHON_GRAPHOBJECT_H #define PYTHON_GRAPHOBJECT_H -#include +#include "preamble.h" + #include #include "structmember.h" #include "common.h" diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index ea3661c3f..f35a102bd 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -20,8 +20,8 @@ */ -#include -#include +#include "preamble.h" + #include #include "arpackobject.h" #include "attributes.h" @@ -822,14 +822,14 @@ PyObject* PyInit__igraph(void) INITERROR; /* Initialize Graph, BFSIter, ARPACKOptions etc */ + if (igraphmodule_ARPACKOptions_register_type()) + INITERROR; if (PyType_Ready(&igraphmodule_GraphType) < 0) INITERROR; if (PyType_Ready(&igraphmodule_BFSIterType) < 0) INITERROR; if (PyType_Ready(&igraphmodule_DFSIterType) < 0) INITERROR; - if (PyType_Ready(&igraphmodule_ARPACKOptionsType) < 0) - INITERROR; /* Initialize the core module */ m = PyModule_Create(&moduledef); @@ -843,7 +843,7 @@ PyObject* PyInit__igraph(void) PyModule_AddObject(m, "GraphBase", (PyObject*)&igraphmodule_GraphType); PyModule_AddObject(m, "BFSIter", (PyObject*)&igraphmodule_BFSIterType); PyModule_AddObject(m, "DFSIter", (PyObject*)&igraphmodule_DFSIterType); - PyModule_AddObject(m, "ARPACKOptions", (PyObject*)&igraphmodule_ARPACKOptionsType); + PyModule_AddObject(m, "ARPACKOptions", (PyObject*)igraphmodule_ARPACKOptionsType); PyModule_AddObject(m, "Edge", (PyObject*)&igraphmodule_EdgeType); PyModule_AddObject(m, "EdgeSeq", (PyObject*)&igraphmodule_EdgeSeqType); PyModule_AddObject(m, "Vertex", (PyObject*)&igraphmodule_VertexType); diff --git a/src/_igraph/indexing.h b/src/_igraph/indexing.h index 105a64f00..6b34ff09c 100644 --- a/src/_igraph/indexing.h +++ b/src/_igraph/indexing.h @@ -24,7 +24,8 @@ #ifndef PYTHON_INDEXING_H #define PYTHON_INDEXING_H -#include +#include "preamble.h" + #include PyObject* igraphmodule_Graph_adjmatrix_get_index(igraph_t* graph, diff --git a/src/_igraph/operators.h b/src/_igraph/operators.h index 3823a55ae..e90dd88e1 100644 --- a/src/_igraph/operators.h +++ b/src/_igraph/operators.h @@ -23,7 +23,7 @@ #ifndef PYTHON_OPERATORS_H #define PYTHON_OPERATORS_H -#include +#include "preamble.h" PyObject* igraphmodule__disjoint_union(PyObject* self, PyObject* args, PyObject* kwds); PyObject* igraphmodule__union(PyObject* self, PyObject* args, PyObject* kwds); diff --git a/src/_igraph/preamble.h b/src/_igraph/preamble.h new file mode 100644 index 000000000..218b304ca --- /dev/null +++ b/src/_igraph/preamble.h @@ -0,0 +1,29 @@ +/* -*- mode: C -*- */ +/* + IGraph library. + Copyright (C) 2006-2021 The igraph development team + + 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 2 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, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA + +*/ + +#ifndef PYTHON_IGRAPH_PREAMBLE_H +#define PYTHON_IGRAPH_PREAMBLE_H + +#define PY_SSIZE_T_CLEAN +#include + +#endif /* PYTHON_IGRAPH_PREAMBLE_H */ diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h index 00bff1d86..9c1f2d876 100644 --- a/src/_igraph/pyhelpers.h +++ b/src/_igraph/pyhelpers.h @@ -24,7 +24,7 @@ #ifndef PYTHON_HELPERS_H #define PYTHON_HELPERS_H -#include +#include "preamble.h" int igraphmodule_PyFile_Close(PyObject* fileObj); PyObject* igraphmodule_PyFile_FromObject(PyObject* filename, const char* mode); diff --git a/src/_igraph/random.h b/src/_igraph/random.h index 5fca144e9..4902e8697 100644 --- a/src/_igraph/random.h +++ b/src/_igraph/random.h @@ -23,7 +23,7 @@ #ifndef PYTHON_RANDOM_H #define PYTHON_RANDOM_H -#include +#include "preamble.h" void igraphmodule_init_rng(PyObject*); PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object); diff --git a/src/_igraph/vertexobject.h b/src/_igraph/vertexobject.h index f33d640eb..abc2d88b6 100644 --- a/src/_igraph/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -23,7 +23,8 @@ #ifndef PYTHON_VERTEXOBJECT_H #define PYTHON_VERTEXOBJECT_H -#include +#include "preamble.h" + #include "graphobject.h" /** diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 17c157945..32709b64d 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -22,7 +22,6 @@ */ -#include #include "attributes.h" #include "common.h" #include "convert.h" diff --git a/src/_igraph/vertexseqobject.h b/src/_igraph/vertexseqobject.h index 987f1f768..38739ff2c 100644 --- a/src/_igraph/vertexseqobject.h +++ b/src/_igraph/vertexseqobject.h @@ -23,7 +23,8 @@ #ifndef PYTHON_VERTEXSEQOBJECT_H #define PYTHON_VERTEXSEQOBJECT_H -#include +#include "preamble.h" + #include "graphobject.h" /** From 50d419b36d9e40ccb6d95296683972a77919c622 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 14:22:54 +0200 Subject: [PATCH 0396/1681] refactor: attributes.c now uses the limited Python API only --- src/_igraph/attributes.c | 219 ++++++++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 37 deletions(-) diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 95375559a..371162cfb 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -20,6 +20,8 @@ */ +#define Py_LIMITED_API 0x03060000 + #include "attributes.h" #include "common.h" #include "convert.h" @@ -73,10 +75,15 @@ int igraphmodule_i_attribute_struct_index_vertex_names( n = PyList_Size(name_list) - 1; while (n >= 0) { - key = PyList_GET_ITEM(name_list, n); /* we don't own a reference to key */ + key = PyList_GetItem(name_list, n); /* we don't own a reference to key */ + if (key == 0) { + return 1; + } + value = PyLong_FromLong(n); /* we do own a reference to value */ - if (value == 0) + if (value == 0) { return 1; + } if (PyDict_SetItem(attrs->vertex_name_index, key, value)) { /* probably unhashable vertex name. If the error is a TypeError, convert @@ -227,7 +234,11 @@ PyObject* igraphmodule_create_edge_attribute(const igraph_t* graph, for (i = 0; i < n; i++) { Py_INCREF(Py_None); - PyList_SET_ITEM(values, i, Py_None); /* reference stolen */ + if (PyList_SetItem(values, i, Py_None)) { /* reference stolen */ + Py_DECREF(values); + Py_DECREF(Py_None); + return 0; + } } if (PyDict_SetItemString(dict, name, values)) { @@ -393,8 +404,8 @@ static int igraphmodule_i_attribute_copy(igraph_t *to, const igraph_t *from, while (PyDict_Next(fromattrs->attrs[i], &pos, &key, &value)) { /* value is only borrowed, so copy it */ if (i>0) { - newval=PyList_New(PyList_GET_SIZE(value)); - for (j=0; jname, value); @@ -789,11 +809,18 @@ static int igraphmodule_i_attribute_permute_edges(const igraph_t *graph, for (i=0; i 0 ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; + item = n > 0 ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[0]) : Py_None; + if (item == 0) { + Py_DECREF(res); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(res, i, item); /* reference to item stolen */ + if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ + Py_DECREF(item); + Py_DECREF(res); + return 0; + } } return res; @@ -1001,14 +1071,26 @@ static PyObject* igraphmodule_i_ac_random(PyObject* values, Py_DECREF(res); return 0; } - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[(long int)(n*PyFloat_AsDouble(num))]); + + item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[(long int)(n*PyFloat_AsDouble(num))]); + if (item == 0) { + Py_DECREF(random_func); + Py_DECREF(res); + return 0; + } + Py_DECREF(num); } else { item = Py_None; } Py_INCREF(item); - PyList_SET_ITEM(res, i, item); /* reference to item stolen */ + if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ + Py_DECREF(item); + Py_DECREF(random_func); + Py_DECREF(res); + return 0; + } } Py_DECREF(random_func); @@ -1032,9 +1114,19 @@ static PyObject* igraphmodule_i_ac_last(PyObject* values, igraph_vector_t *v = (igraph_vector_t*)VECTOR(*merges)[i]; long int n = igraph_vector_size(v); - item = (n > 0) ? PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; + item = (n > 0) ? PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[n-1]) : Py_None; + if (item == 0) { + Py_DECREF(res); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(res, i, item); /* reference to item stolen */ + + if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ + Py_DECREF(item); + Py_DECREF(res); + return 0; + } } return res; @@ -1058,7 +1150,12 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, long int j, n = igraph_vector_size(v); for (j = 0; j < n; ) { - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); + item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); + if (item == 0) { + Py_DECREF(res); + return 0; + } + if (igraphmodule_PyObject_to_real_t(item, &num)) { PyErr_SetString(PyExc_TypeError, "mean can only be invoked on numeric attributes"); Py_DECREF(res); @@ -1070,7 +1167,12 @@ static PyObject* igraphmodule_i_ac_mean(PyObject* values, } /* reference to new float stolen */ - PyList_SET_ITEM(res, i, PyFloat_FromDouble((double)mean)); + item = PyFloat_FromDouble((double)mean); + if (PyList_SetItem(res, i, item)) { /* reference to item stolen */ + Py_DECREF(item); + Py_DECREF(res); + return 0; + } } return res; @@ -1093,40 +1195,78 @@ static PyObject* igraphmodule_i_ac_median(PyObject* values, long int j, n = igraph_vector_size(v); list = PyList_New(n); for (j = 0; j < n; j++) { - item = PyList_GET_ITEM(values, (Py_ssize_t)VECTOR(*v)[j]); + item = PyList_GetItem(values, (Py_ssize_t)VECTOR(*v)[j]); + if (item == 0) { + Py_DECREF(res); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(list, j, item); /* reference to item stolen */ + if (PyList_SetItem(list, j, item)) { /* reference to item stolen */ + Py_DECREF(item); + Py_DECREF(list); + Py_DECREF(res); + return 0; + } } + /* sort the list */ if (PyList_Sort(list)) { Py_DECREF(list); Py_DECREF(res); return 0; } + if (n == 0) { item = Py_None; Py_INCREF(item); } else if (n % 2 == 1) { - item = PyList_GET_ITEM(list, n / 2); + item = PyList_GetItem(list, n / 2); + if (item == 0) { + Py_DECREF(list); + Py_DECREF(res); + return 0; + } + Py_INCREF(item); } else { igraph_real_t num1, num2; - item = PyList_GET_ITEM(list, n / 2 - 1); + item = PyList_GetItem(list, n / 2 - 1); + if (item == 0) { + Py_DECREF(list); + Py_DECREF(res); + return 0; + } + if (igraphmodule_PyObject_to_real_t(item, &num1)) { Py_DECREF(list); Py_DECREF(res); return 0; } - item = PyList_GET_ITEM(list, n / 2); + + item = PyList_GetItem(list, n / 2); + if (item == 0) { + Py_DECREF(list); + Py_DECREF(res); + return 0; + } + if (igraphmodule_PyObject_to_real_t(item, &num2)) { Py_DECREF(list); Py_DECREF(res); return 0; } + item = PyFloat_FromDouble((num1 + num2) / 2); } + /* reference to item stolen */ - PyList_SET_ITEM(res, i, item); + if (PyList_SetItem(res, i, item)) { + Py_DECREF(item); + Py_DECREF(list); + Py_DECREF(res); + return 0; + } } return res; @@ -1443,16 +1583,16 @@ int igraphmodule_i_attribute_get_type(const igraph_t *graph, if (attrnum>0) { for (i=0; i Date: Fri, 10 Sep 2021 14:23:56 +0200 Subject: [PATCH 0397/1681] feat: test.sh now allows cleaning the compiled igraph C core --- test.sh | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index 007cc8cef..d5c4193cb 100755 --- a/test.sh +++ b/test.sh @@ -7,14 +7,31 @@ PYTHON=python3 set -e +CLEAN=0 + +while getopts ":c" OPTION; do + case $OPTION in + c) + CLEAN=1 + ;; + \?) + echo "Usage: $0 [-c]" + ;; + esac +done +shift $((OPTIND -1)) + if [ ! -d $VENV_DIR ]; then - $PYTHON -m venv $VENV_DIR + $PYTHON -m venv $VENV_DIR $VENV_DIR/bin/pip install -U pip wheel fi rm -rf build/ +if [ x$CLEAN = x1 ]; then + rm -rf vendor/build vendor/install +fi + $VENV_DIR/bin/python setup.py build $VENV_DIR/bin/pip install --use-feature=in-tree-build .[plotting,test] $VENV_DIR/bin/pytest tests - From 3d903100d4d01fc817c3d9ee441d3bb95a84c5f8 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 15:10:25 +0200 Subject: [PATCH 0398/1681] refactor: BFSIter and DFSIter now use ony the Python limited API --- src/_igraph/arpackobject.c | 41 ++++++------- src/_igraph/arpackobject.h | 2 +- src/_igraph/bfsiter.c | 118 ++++++++++++++----------------------- src/_igraph/bfsiter.h | 16 ++--- src/_igraph/dfsiter.c | 103 +++++++++++--------------------- src/_igraph/dfsiter.h | 16 ++--- src/_igraph/graphobject.c | 14 +---- src/_igraph/igraphmodule.c | 15 ++--- 8 files changed, 128 insertions(+), 197 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 3e160e9f8..c4e020450 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -208,28 +208,25 @@ PyDoc_STRVAR( " - C{numreo}: total number of steps of re-orthogonalization\n\n" ); -PyType_Slot igraphmodule_ARPACKOptions_slots[] = { - { Py_tp_new, igraphmodule_ARPACKOptions_new }, - { Py_tp_dealloc, igraphmodule_ARPACKOptions_dealloc }, - { Py_tp_getattr, igraphmodule_ARPACKOptions_getattr }, - { Py_tp_setattr, igraphmodule_ARPACKOptions_setattr }, - { Py_tp_str, igraphmodule_ARPACKOptions_str }, - { Py_tp_doc, (void*) igraphmodule_ARPACKOptions_doc }, - { 0 } -}; - -/** \ingroup python_interface_arpack - * Python type specification for \c igraph.ARPACKOptions - */ -PyType_Spec igraphmodule_ARPACKOptions_spec = { - "igraph.ARPACKOptions", /* name */ - sizeof(igraphmodule_ARPACKOptionsObject), /* basicsize */ - 0, /* itemsize */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ - igraphmodule_ARPACKOptions_slots, /* slots */ -}; - int igraphmodule_ARPACKOptions_register_type() { - igraphmodule_ARPACKOptionsType = (PyTypeObject*) PyType_FromSpec(&igraphmodule_ARPACKOptions_spec); + PyType_Slot slots[] = { + { Py_tp_new, igraphmodule_ARPACKOptions_new }, + { Py_tp_dealloc, igraphmodule_ARPACKOptions_dealloc }, + { Py_tp_getattr, igraphmodule_ARPACKOptions_getattr }, + { Py_tp_setattr, igraphmodule_ARPACKOptions_setattr }, + { Py_tp_str, igraphmodule_ARPACKOptions_str }, + { Py_tp_doc, (void*) igraphmodule_ARPACKOptions_doc }, + { 0 } + }; + + PyType_Spec spec = { + "igraph.ARPACKOptions", /* name */ + sizeof(igraphmodule_ARPACKOptionsObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ + slots, /* slots */ + }; + + igraphmodule_ARPACKOptionsType = (PyTypeObject*) PyType_FromSpec(&spec); return igraphmodule_ARPACKOptionsType == 0; } diff --git a/src/_igraph/arpackobject.h b/src/_igraph/arpackobject.h index 5d5fd31d0..ea482444b 100644 --- a/src/_igraph/arpackobject.h +++ b/src/_igraph/arpackobject.h @@ -47,7 +47,7 @@ typedef struct { extern PyTypeObject* igraphmodule_ARPACKOptionsType; extern PyObject* igraphmodule_arpack_options_default; -int igraphmodule_ARPACKOptions_register_type(); +int igraphmodule_ARPACKOptions_register_type(void); PyObject* igraphmodule_ARPACKOptions_new(void); igraph_arpack_options_t *igraphmodule_ARPACKOptions_get(igraphmodule_ARPACKOptionsObject *self); diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 03615ceb9..eafaa3c06 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -20,6 +20,8 @@ */ +#define Py_LIMITED_API 0x03060000 + #include "bfsiter.h" #include "common.h" #include "convert.h" @@ -31,7 +33,7 @@ * \defgroup python_interface_bfsiter BFS iterator object */ -PyTypeObject igraphmodule_BFSIterType; +PyTypeObject* igraphmodule_BFSIterType; /** * \ingroup python_interface_bfsiter @@ -45,10 +47,10 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraphmodule_BFSIterObject* o; igraph_integer_t no_of_nodes, r; - o = PyObject_GC_New(igraphmodule_BFSIterObject, &igraphmodule_BFSIterType); + o = PyObject_GC_New(igraphmodule_BFSIterObject, igraphmodule_BFSIterType); Py_INCREF(g); - o->gref=g; - o->graph=&g->g; + o->gref = g; + o->graph = &g->g; if (!PyLong_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); @@ -56,7 +58,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, } no_of_nodes = igraph_vcount(&g->g); - o->visited=(char*)calloc(no_of_nodes, sizeof(char)); + o->visited = (char*)calloc(no_of_nodes, sizeof(char)); if (o->visited == 0) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; @@ -91,14 +93,14 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - o->visited[r]=1; + o->visited[r] = 1; if (!igraph_is_directed(&g->g)) { mode=IGRAPH_ALL; } - o->mode=mode; - o->advanced=advanced; + o->mode = mode; + o->advanced = advanced; PyObject_GC_Track(o); @@ -116,15 +118,9 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, */ int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, visitproc visit, void *arg) { - int vret; - RC_TRAVERSE("BFSIter", self); - - if (self->gref) { - vret=visit((PyObject*)self->gref, arg); - if (vret != 0) return vret; - } - + Py_VISIT(self->gref); + Py_VISIT(Py_TYPE(self)); /* needed because heap-allocated types are refcounted */ return 0; } @@ -137,12 +133,13 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { PyObject_GC_UnTrack(self); - tmp=(PyObject*)self->gref; - self->gref=NULL; + tmp = (PyObject*)self->gref; + self->gref = NULL; Py_XDECREF(tmp); igraph_dqueue_int_destroy(&self->queue); igraph_vector_int_destroy(&self->neis); + free(self->visited); self->visited=0; @@ -154,11 +151,14 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { * \brief Deallocates a Python representation of a given BFS iterator object */ void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { + PyTypeObject *tp = Py_TYPE(self); + igraphmodule_BFSIter_clear(self); RC_DEALLOC("BFSIter", self); PyObject_GC_Del(self); + Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { @@ -214,58 +214,30 @@ PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { } } -/** - * \ingroup python_interface_bfsiter - * Method table for the \c igraph.BFSIter object - */ -PyMethodDef igraphmodule_BFSIter_methods[] = { - {NULL} -}; - -/** \ingroup python_interface_bfsiter - * Python type object referencing the methods Python calls when it performs various operations on - * a BFS iterator of a graph - */ -PyTypeObject igraphmodule_BFSIterType = -{ - PyVarObject_HEAD_INIT(0, 0) - "igraph.BFSIter", // tp_name - sizeof(igraphmodule_BFSIterObject), // tp_basicsize - 0, // tp_itemsize - (destructor)igraphmodule_BFSIter_dealloc, // tp_dealloc - 0, // tp_print - 0, // tp_getattr - 0, // tp_setattr - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - 0, // tp_repr - 0, // tp_as_number - 0, // tp_as_sequence - 0, // tp_as_mapping - 0, // tp_hash - 0, // tp_call - 0, // tp_str - 0, // tp_getattro - 0, // tp_setattro - 0, // tp_as_buffer - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, // tp_flags - "igraph BFS iterator object", // tp_doc - (traverseproc) igraphmodule_BFSIter_traverse, /* tp_traverse */ - (inquiry) igraphmodule_BFSIter_clear, /* tp_clear */ - 0, // tp_richcompare - 0, // tp_weaklistoffset - (getiterfunc)igraphmodule_BFSIter_iter, /* tp_iter */ - (iternextfunc)igraphmodule_BFSIter_iternext, /* tp_iternext */ - 0, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ - 0, /* tp_free */ -}; - +PyDoc_STRVAR( + igraphmodule_BFSIter_doc, + "igraph BFS iterator object" +); + +int igraphmodule_BFSIter_register_type() { + PyType_Slot slots[] = { + { Py_tp_dealloc, igraphmodule_BFSIter_dealloc }, + { Py_tp_traverse, igraphmodule_BFSIter_traverse }, + { Py_tp_clear, igraphmodule_BFSIter_clear }, + { Py_tp_iter, igraphmodule_BFSIter_iter }, + { Py_tp_iternext, igraphmodule_BFSIter_iternext }, + { Py_tp_doc, (void*) igraphmodule_BFSIter_doc }, + { 0 } + }; + + PyType_Spec spec = { + "igraph.BFSIter", /* name */ + sizeof(igraphmodule_BFSIterObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* flags */ + slots, /* slots */ + }; + + igraphmodule_BFSIterType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_BFSIterType == 0; +} diff --git a/src/_igraph/bfsiter.h b/src/_igraph/bfsiter.h index 2295963a9..60469c011 100644 --- a/src/_igraph/bfsiter.h +++ b/src/_igraph/bfsiter.h @@ -31,8 +31,7 @@ * \ingroup python_interface_bfsiter * \brief A structure representing a BFS iterator of a graph */ -typedef struct -{ +typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; igraph_dqueue_int_t queue; @@ -43,12 +42,13 @@ typedef struct igraph_bool_t advanced; } igraphmodule_BFSIterObject; -PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *o, igraph_neimode_t mode, igraph_bool_t advanced); -int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, - visitproc visit, void *arg); -int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self); -void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self); +extern PyTypeObject* igraphmodule_BFSIterType; -extern PyTypeObject igraphmodule_BFSIterType; +int igraphmodule_BFSIter_register_type(void); + +PyObject* igraphmodule_BFSIter_new( + igraphmodule_GraphObject *g, PyObject *o, igraph_neimode_t mode, + igraph_bool_t advanced +); #endif diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index dbbf1c546..3f4ca8f06 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -20,6 +20,8 @@ */ +#define Py_LIMITED_API 0x03060000 + #include "convert.h" #include "dfsiter.h" #include "common.h" @@ -31,7 +33,7 @@ * \defgroup python_interface_dfsiter DFS iterator object */ -PyTypeObject igraphmodule_DFSIterType; +PyTypeObject* igraphmodule_DFSIterType; /** * \ingroup python_interface_dfsiter @@ -45,10 +47,10 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraphmodule_DFSIterObject* o; igraph_integer_t no_of_nodes, r; - o=PyObject_GC_New(igraphmodule_DFSIterObject, &igraphmodule_DFSIterType); + o = PyObject_GC_New(igraphmodule_DFSIterObject, igraphmodule_DFSIterType); Py_INCREF(g); - o->gref=g; - o->graph=&g->g; + o->gref = g; + o->graph = &g->g; if (!PyLong_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); @@ -117,17 +119,9 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, */ int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, visitproc visit, void *arg) { - int vret; - RC_TRAVERSE("DFSIter", self); - - if (self->gref) { - vret = visit((PyObject*)self->gref, arg); - if (vret != 0) { - return vret; - } - } - + Py_VISIT(self->gref); + Py_VISIT(Py_TYPE(self)); /* needed because heap-allocated types are refcounted */ return 0; } @@ -157,11 +151,14 @@ int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { * \brief Deallocates a Python representation of a given DFS iterator object */ void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { + PyTypeObject *tp = Py_TYPE(self); + igraphmodule_DFSIter_clear(self); RC_DEALLOC("DFSIter", self); PyObject_GC_Del(self); + Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { @@ -252,58 +249,30 @@ PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { } } -/** - * \ingroup python_interface_dfsiter - * Method table for the \c igraph.DFSIter object - */ -PyMethodDef igraphmodule_DFSIter_methods[] = { - {NULL} -}; +PyDoc_STRVAR( + igraphmodule_DFSIter_doc, + "igraph DFS iterator object" +); -/** \ingroup python_interface_dfsiter - * Python type object referencing the methods Python calls when it performs various operations on - * a DFS iterator of a graph - */ -PyTypeObject igraphmodule_DFSIterType = -{ - PyVarObject_HEAD_INIT(0, 0) - "igraph.DFSIter", // tp_name - sizeof(igraphmodule_DFSIterObject), // tp_basicsize - 0, // tp_itemsize - (destructor)igraphmodule_DFSIter_dealloc, // tp_dealloc - 0, // tp_print - 0, // tp_getattr - 0, // tp_setattr - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - 0, // tp_repr - 0, // tp_as_number - 0, // tp_as_sequence - 0, // tp_as_mapping - 0, // tp_hash - 0, // tp_call - 0, // tp_str - 0, // tp_getattro - 0, // tp_setattro - 0, // tp_as_buffer - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, // tp_flags - "igraph DFS iterator object", // tp_doc - (traverseproc) igraphmodule_DFSIter_traverse, /* tp_traverse */ - (inquiry) igraphmodule_DFSIter_clear, /* tp_clear */ - 0, // tp_richcompare - 0, // tp_weaklistoffset - (getiterfunc)igraphmodule_DFSIter_iter, /* tp_iter */ - (iternextfunc)igraphmodule_DFSIter_iternext, /* tp_iternext */ - 0, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - 0, /* tp_new */ - 0, /* tp_free */ -}; +int igraphmodule_DFSIter_register_type() { + PyType_Slot slots[] = { + { Py_tp_dealloc, igraphmodule_DFSIter_dealloc }, + { Py_tp_traverse, igraphmodule_DFSIter_traverse }, + { Py_tp_clear, igraphmodule_DFSIter_clear }, + { Py_tp_iter, igraphmodule_DFSIter_iter }, + { Py_tp_iternext, igraphmodule_DFSIter_iternext }, + { Py_tp_doc, (void*) igraphmodule_DFSIter_doc }, + { 0 } + }; + + PyType_Spec spec = { + "igraph.DFSIter", /* name */ + sizeof(igraphmodule_DFSIterObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* flags */ + slots, /* slots */ + }; + igraphmodule_DFSIterType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_DFSIterType == 0; +} diff --git a/src/_igraph/dfsiter.h b/src/_igraph/dfsiter.h index 5e8c74f63..1b21c7139 100644 --- a/src/_igraph/dfsiter.h +++ b/src/_igraph/dfsiter.h @@ -31,8 +31,7 @@ * \ingroup python_interface_dfsiter * \brief A structure representing a DFS iterator of a graph */ -typedef struct -{ +typedef struct { PyObject_HEAD igraphmodule_GraphObject* gref; igraph_stack_int_t stack; @@ -43,12 +42,13 @@ typedef struct igraph_bool_t advanced; } igraphmodule_DFSIterObject; -PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *o, igraph_neimode_t mode, igraph_bool_t advanced); -int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, - visitproc visit, void *arg); -int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self); -void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self); +extern PyTypeObject* igraphmodule_DFSIterType; -extern PyTypeObject igraphmodule_DFSIterType; +int igraphmodule_DFSIter_register_type(void); + +PyObject* igraphmodule_DFSIter_new( + igraphmodule_GraphObject *g, PyObject *o, igraph_neimode_t mode, + igraph_bool_t advanced +); #endif diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index d58d327cb..1989fabea 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -133,21 +133,13 @@ int igraphmodule_Graph_clear(igraphmodule_GraphObject * self) int igraphmodule_Graph_traverse(igraphmodule_GraphObject * self, visitproc visit, void *arg) { - int vret, i; - RC_TRAVERSE("Graph", self); - if (self->destructor) { - vret = visit(self->destructor, arg); - if (vret != 0) - return vret; - } + Py_VISIT(self->destructor); if (self->g.attr) { - for (i = 0; i < 3; i++) { - vret = visit(((PyObject **) (self->g.attr))[i], arg); - if (vret != 0) - return vret; + for (int i = 0; i < 3; i++) { + Py_VISIT(((PyObject**)self->g.attr)[i]); } } diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 374e1bd62..728601a1c 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -852,14 +852,15 @@ PyObject* PyInit__igraph(void) INITERROR; /* Initialize Graph, BFSIter, ARPACKOptions etc */ - if (igraphmodule_ARPACKOptions_register_type()) + if ( + igraphmodule_ARPACKOptions_register_type() || + igraphmodule_BFSIter_register_type() || + igraphmodule_DFSIter_register_type() + ) { INITERROR; + } if (PyType_Ready(&igraphmodule_GraphType) < 0) INITERROR; - if (PyType_Ready(&igraphmodule_BFSIterType) < 0) - INITERROR; - if (PyType_Ready(&igraphmodule_DFSIterType) < 0) - INITERROR; /* Initialize the core module */ m = PyModule_Create(&moduledef); @@ -871,8 +872,8 @@ PyObject* PyInit__igraph(void) /* Add the types to the core module */ PyModule_AddObject(m, "GraphBase", (PyObject*)&igraphmodule_GraphType); - PyModule_AddObject(m, "BFSIter", (PyObject*)&igraphmodule_BFSIterType); - PyModule_AddObject(m, "DFSIter", (PyObject*)&igraphmodule_DFSIterType); + PyModule_AddObject(m, "BFSIter", (PyObject*)igraphmodule_BFSIterType); + PyModule_AddObject(m, "DFSIter", (PyObject*)igraphmodule_DFSIterType); PyModule_AddObject(m, "ARPACKOptions", (PyObject*)igraphmodule_ARPACKOptionsType); PyModule_AddObject(m, "Edge", (PyObject*)&igraphmodule_EdgeType); PyModule_AddObject(m, "EdgeSeq", (PyObject*)&igraphmodule_EdgeSeqType); From 085bf11f1ee580e96c6fca74a7f06a181f8394a9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 15:54:37 +0200 Subject: [PATCH 0399/1681] refactor: Edge and Vertex now use the limited Python API only --- src/_igraph/bfsiter.c | 10 +- src/_igraph/convert.c | 4 +- src/_igraph/dfsiter.c | 12 +-- src/_igraph/edgeobject.c | 149 +++++++++++++++--------------- src/_igraph/edgeobject.h | 14 +-- src/_igraph/igraphmodule.c | 19 ++-- src/_igraph/vertexobject.c | 182 +++++++++++++++++++++---------------- src/_igraph/vertexobject.h | 15 +-- 8 files changed, 203 insertions(+), 202 deletions(-) diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index eafaa3c06..0f8dffd40 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -52,7 +52,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, o->gref = g; o->graph = &g->g; - if (!PyLong_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { + if (!PyLong_Check(root) && !igraphmodule_Vertex_Check(root)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); return NULL; } @@ -116,7 +116,7 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, * This is necessary because the \c igraph.BFSIter object contains several * other \c PyObject pointers and they might point back to itself. */ -int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, +static int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("BFSIter", self); Py_VISIT(self->gref); @@ -150,7 +150,7 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { * \ingroup python_interface_bfsiter * \brief Deallocates a Python representation of a given BFS iterator object */ -void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { +static void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { PyTypeObject *tp = Py_TYPE(self); igraphmodule_BFSIter_clear(self); @@ -161,12 +161,12 @@ void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } -PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { +static PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { Py_INCREF(self); return (PyObject*)self; } -PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { +static PyObject* igraphmodule_BFSIter_iternext(igraphmodule_BFSIterObject* self) { if (!igraph_dqueue_int_empty(&self->queue)) { igraph_integer_t vid = igraph_dqueue_int_pop(&self->queue); igraph_integer_t dist = igraph_dqueue_int_pop(&self->queue); diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index aa40b9115..2d9234328 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2760,7 +2760,7 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g if (igraphmodule_get_vertex_id_by_name(graph, o, vid)) { return 1; } - } else if (PyObject_IsInstance(o, (PyObject*)&igraphmodule_VertexType)) { + } else if (igraphmodule_Vertex_Check(o)) { /* Single vertex ID from Vertex object */ igraphmodule_VertexObject *vo = (igraphmodule_VertexObject*)o; *vid = igraphmodule_Vertex_get_index_igraph_integer(vo); @@ -2971,7 +2971,7 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g /* Single edge ID */ if (igraphmodule_PyObject_to_integer_t(o, eid)) return 1; - } else if (PyObject_IsInstance(o, (PyObject*)&igraphmodule_EdgeType)) { + } else if (igraphmodule_Edge_Check(o)) { /* Single edge ID from Edge object */ igraphmodule_EdgeObject *eo = (igraphmodule_EdgeObject*)o; *eid = igraphmodule_Edge_get_index_as_igraph_integer(eo); diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 3f4ca8f06..d1e419d7c 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -52,7 +52,7 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, o->gref = g; o->graph = &g->g; - if (!PyLong_Check(root) && !PyObject_IsInstance(root, (PyObject*)&igraphmodule_VertexType)) { + if (!PyLong_Check(root) && !igraphmodule_Vertex_Check(root)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); return NULL; } @@ -117,7 +117,7 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, * This is necessary because the \c igraph.DFSIter object contains several * other \c PyObject pointers and they might point back to itself. */ -int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, +static int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("DFSIter", self); Py_VISIT(self->gref); @@ -129,7 +129,7 @@ int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, * \ingroup python_interface_dfsiter * \brief Clears the iterator's subobject (before deallocation) */ -int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { +static int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { PyObject *tmp; PyObject_GC_UnTrack(self); @@ -150,7 +150,7 @@ int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { * \ingroup python_interface_dfsiter * \brief Deallocates a Python representation of a given DFS iterator object */ -void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { +static void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { PyTypeObject *tp = Py_TYPE(self); igraphmodule_DFSIter_clear(self); @@ -161,12 +161,12 @@ void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } -PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { +static PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { Py_INCREF(self); return (PyObject*)self; } -PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { +static PyObject* igraphmodule_DFSIter_iternext(igraphmodule_DFSIterObject* self) { /* the design is to return the top of the stack and then proceed until * we have found an unvisited neighbor and push that on top */ igraph_integer_t parent_out, dist_out, vid_out; diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 7410412a6..b0ef72d3b 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -22,6 +22,8 @@ */ +#define Py_LIMITED_API 0x03060000 + #include "attributes.h" #include "convert.h" #include "edgeobject.h" @@ -35,17 +37,16 @@ * \defgroup python_interface_edge Edge object */ -PyTypeObject igraphmodule_EdgeType; +PyTypeObject* igraphmodule_EdgeType; + +PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self); /** * \ingroup python_interface_edge * \brief Checks whether the given Python object is an edge */ int igraphmodule_Edge_Check(PyObject* obj) { - if (!obj) - return 0; - - return PyObject_IsInstance(obj, (PyObject*)(&igraphmodule_EdgeType)); + return obj ? PyObject_IsInstance(obj, (PyObject*)(igraphmodule_EdgeType)) : 0; } /** @@ -101,13 +102,13 @@ int igraphmodule_Edge_Validate(PyObject* obj) { */ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { igraphmodule_EdgeObject* self; - self=PyObject_New(igraphmodule_EdgeObject, &igraphmodule_EdgeType); + self = PyObject_New(igraphmodule_EdgeObject, igraphmodule_EdgeType); if (self) { RC_ALLOC("Edge", self); Py_INCREF(gref); - self->gref=gref; - self->idx=idx; - self->hash=-1; + self->gref = gref; + self->idx = idx; + self->hash = -1; } return (PyObject*)self; } @@ -130,12 +131,15 @@ int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { * \ingroup python_interface_edge * \brief Deallocates a Python representation of a given edge object */ -void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { +static void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { + PyTypeObject* tp = Py_TYPE(self); + igraphmodule_Edge_clear(self); RC_DEALLOC("Edge", self); PyObject_Del((PyObject*)self); + Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } /** \ingroup python_interface_edge @@ -143,7 +147,7 @@ void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { * * \return the formatted textual representation as a \c PyObject */ -PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self) { +static PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self) { PyObject *s; PyObject *attrs; @@ -161,7 +165,7 @@ PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self) { /** \ingroup python_interface_edge * \brief Returns the hash code of the edge */ -long igraphmodule_Edge_hash(igraphmodule_EdgeObject* self) { +static long igraphmodule_Edge_hash(igraphmodule_EdgeObject* self) { long hash_graph; long hash_index; long result; @@ -198,7 +202,7 @@ long igraphmodule_Edge_hash(igraphmodule_EdgeObject* self) { /** \ingroup python_interface_edge * \brief Rich comparison of an edge with another */ -PyObject* igraphmodule_Edge_richcompare(igraphmodule_EdgeObject *a, +static PyObject* igraphmodule_Edge_richcompare(igraphmodule_EdgeObject *a, PyObject *b, int op) { igraphmodule_EdgeObject* self = a; @@ -242,17 +246,18 @@ PyObject* igraphmodule_Edge_richcompare(igraphmodule_EdgeObject *a, Py_ssize_t igraphmodule_Edge_attribute_count(igraphmodule_EdgeObject* self) { igraphmodule_GraphObject *o = self->gref; - if (!o) return 0; - if (!((PyObject**)o->g.attr)[1]) return 0; - return PyDict_Size(((PyObject**)o->g.attr)[1]); + if (!o || !((PyObject**)o->g.attr)[1]) { + return 0; + } else { + return PyDict_Size(((PyObject**)o->g.attr)[1]); + } } /** \ingroup python_interface_edge * \brief Returns the list of attribute names */ PyObject* igraphmodule_Edge_attribute_names(igraphmodule_EdgeObject* self) { - if (!self->gref) return NULL; - return igraphmodule_Graph_edge_attributes(self->gref); + return self->gref ? igraphmodule_Graph_edge_attributes(self->gref) : 0; } /** \ingroup python_interface_edge @@ -290,7 +295,15 @@ PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self) { /* no need to Py_INCREF, PyDict_SetItem will do that */ PyDict_SetItem(dict, name, value); } + } else { + Py_DECREF(dict); + Py_DECREF(names); + return NULL; } + } else { + Py_DECREF(dict); + Py_DECREF(names); + return NULL; } } @@ -584,18 +597,25 @@ PyObject* igraphmodule_Edge_get_graph(igraphmodule_EdgeObject* self, void* closu #define GRAPH_PROXY_METHOD(FUNC, METHODNAME) \ PyObject* igraphmodule_Edge_##FUNC(igraphmodule_EdgeObject* self, PyObject* args, PyObject* kwds) { \ PyObject *new_args, *item, *result; \ - Py_ssize_t i, num_args = args ? PyTuple_Size(args)+1 : 1; \ + Py_ssize_t i, num_args = args ? PyTuple_Size(args) + 1 : 1; \ \ /* Prepend ourselves to args */ \ new_args = PyTuple_New(num_args); \ - Py_INCREF(self); PyTuple_SET_ITEM(new_args, 0, (PyObject*)self); \ + Py_INCREF(self); \ + PyTuple_SetItem(new_args, 0, (PyObject*)self); \ for (i = 1; i < num_args; i++) { \ - item = PyTuple_GET_ITEM(args, i-1); \ - Py_INCREF(item); PyTuple_SET_ITEM(new_args, i, item); \ + item = PyTuple_GetItem(args, i - 1); \ + Py_INCREF(item); \ + PyTuple_SetItem(new_args, i, item); \ } \ \ /* Get the method instance */ \ item = PyObject_GetAttrString((PyObject*)(self->gref), METHODNAME); \ + if (item == 0) { \ + Py_DECREF(new_args); \ + return 0; \ + } \ + \ result = PyObject_Call(item, new_args, kwds); \ Py_DECREF(item); \ Py_DECREF(new_args); \ @@ -691,46 +711,8 @@ PyGetSetDef igraphmodule_Edge_getseters[] = { {NULL} }; -/** \ingroup python_interface_edge - * This structure is the collection of functions necessary to implement - * the edge as a mapping (i.e. to allow the retrieval and setting of - * igraph attributes in Python as if it were of a Python mapping type) - */ -PyMappingMethods igraphmodule_Edge_as_mapping = { - // returns the number of edge attributes - (lenfunc)igraphmodule_Edge_attribute_count, - // returns an attribute by name - (binaryfunc)igraphmodule_Edge_get_attribute, - // sets an attribute by name - (objobjargproc)igraphmodule_Edge_set_attribute -}; - -/** \ingroup python_interface_edge - * Python type object referencing the methods Python calls when it performs various operations on - * an edge of a graph - */ -PyTypeObject igraphmodule_EdgeType = -{ - PyVarObject_HEAD_INIT(0, 0) - "igraph.Edge", // tp_name - sizeof(igraphmodule_EdgeObject), // tp_basicsize - 0, // tp_itemsize - (destructor)igraphmodule_Edge_dealloc, // tp_dealloc - 0, // tp_print - 0, // tp_getattr - 0, // tp_setattr - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - (reprfunc)igraphmodule_Edge_repr, // tp_repr - 0, // tp_as_number - 0, // tp_as_sequence - &igraphmodule_Edge_as_mapping, // tp_as_mapping - (hashfunc)igraphmodule_Edge_hash, /* tp_hash */ - 0, // tp_call - 0, // tp_str - 0, // tp_getattro - 0, // tp_setattro - 0, // tp_as_buffer - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // tp_flags +PyDoc_STRVAR( + igraphmodule_Edge_doc, "Class representing a single edge in a graph.\n\n" "The edge is referenced by its index, so if the underlying graph\n" "changes, the semantics of the edge object might change as well\n" @@ -739,15 +721,34 @@ PyTypeObject igraphmodule_EdgeType = "as a hash:\n\n" " >>> e[\"weight\"] = 2 #doctest: +SKIP\n" " >>> print(e[\"weight\"]) #doctest: +SKIP\n" - " 2\n", // tp_doc - 0, // tp_traverse - 0, // tp_clear - (richcmpfunc)igraphmodule_Edge_richcompare, /* tp_richcompare */ - 0, // tp_weaklistoffset - 0, // tp_iter - 0, // tp_iternext - igraphmodule_Edge_methods, // tp_methods - 0, // tp_members - igraphmodule_Edge_getseters, // tp_getset -}; - + " 2\n" +); + +int igraphmodule_Edge_register_type() { + PyType_Slot slots[] = { + { Py_tp_dealloc, igraphmodule_Edge_dealloc }, + { Py_tp_hash, igraphmodule_Edge_hash }, + { Py_tp_repr, igraphmodule_Edge_repr }, + { Py_tp_richcompare, igraphmodule_Edge_richcompare }, + { Py_tp_methods, igraphmodule_Edge_methods }, + { Py_tp_getset, igraphmodule_Edge_getseters }, + { Py_tp_doc, (void*) igraphmodule_Edge_doc }, + + { Py_mp_length, igraphmodule_Edge_attribute_count }, + { Py_mp_subscript, igraphmodule_Edge_get_attribute }, + { Py_mp_ass_subscript, igraphmodule_Edge_set_attribute }, + + { 0 } + }; + + PyType_Spec spec = { + "igraph.Edge", /* name */ + sizeof(igraphmodule_EdgeObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ + slots, /* slots */ + }; + + igraphmodule_EdgeType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_EdgeType == 0; +} diff --git a/src/_igraph/edgeobject.h b/src/_igraph/edgeobject.h index b2192674b..7432d22a3 100644 --- a/src/_igraph/edgeobject.h +++ b/src/_igraph/edgeobject.h @@ -38,20 +38,12 @@ typedef struct { long hash; } igraphmodule_EdgeObject; -int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self); -void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self); +extern PyTypeObject* igraphmodule_EdgeType; -int igraphmodule_Edge_Check(PyObject *obj); -int igraphmodule_Edge_Validate(PyObject *obj); +int igraphmodule_Edge_register_type(void); +int igraphmodule_Edge_Check(PyObject* obj); PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx); -PyObject* igraphmodule_Edge_repr(igraphmodule_EdgeObject *self); -PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self); -PyObject* igraphmodule_Edge_attribute_names(igraphmodule_EdgeObject* self); igraph_integer_t igraphmodule_Edge_get_index_as_igraph_integer(igraphmodule_EdgeObject* self); -PyObject* igraphmodule_Edge_update_attributes(PyObject* self, PyObject* args, - PyObject* kwds); - -extern PyTypeObject igraphmodule_EdgeType; #endif diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 728601a1c..0fad4c359 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -842,20 +842,13 @@ PyObject* PyInit__igraph(void) if (PyType_Ready(&igraphmodule_EdgeSeqType) < 0) INITERROR; - /* Initialize Vertex, Edge */ - igraphmodule_VertexType.tp_clear = (inquiry)igraphmodule_Vertex_clear; - if (PyType_Ready(&igraphmodule_VertexType) < 0) - INITERROR; - - igraphmodule_EdgeType.tp_clear = (inquiry)igraphmodule_Edge_clear; - if (PyType_Ready(&igraphmodule_EdgeType) < 0) - INITERROR; - - /* Initialize Graph, BFSIter, ARPACKOptions etc */ + /* Initialize types */ if ( igraphmodule_ARPACKOptions_register_type() || igraphmodule_BFSIter_register_type() || - igraphmodule_DFSIter_register_type() + igraphmodule_DFSIter_register_type() || + igraphmodule_Edge_register_type() || + igraphmodule_Vertex_register_type() ) { INITERROR; } @@ -875,9 +868,9 @@ PyObject* PyInit__igraph(void) PyModule_AddObject(m, "BFSIter", (PyObject*)igraphmodule_BFSIterType); PyModule_AddObject(m, "DFSIter", (PyObject*)igraphmodule_DFSIterType); PyModule_AddObject(m, "ARPACKOptions", (PyObject*)igraphmodule_ARPACKOptionsType); - PyModule_AddObject(m, "Edge", (PyObject*)&igraphmodule_EdgeType); + PyModule_AddObject(m, "Edge", (PyObject*)igraphmodule_EdgeType); PyModule_AddObject(m, "EdgeSeq", (PyObject*)&igraphmodule_EdgeSeqType); - PyModule_AddObject(m, "Vertex", (PyObject*)&igraphmodule_VertexType); + PyModule_AddObject(m, "Vertex", (PyObject*)igraphmodule_VertexType); PyModule_AddObject(m, "VertexSeq", (PyObject*)&igraphmodule_VertexSeqType); /* Internal error exception type */ diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 71dc24c9b..699634789 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -22,6 +22,8 @@ */ +#define Py_LIMITED_API 0x03060000 + #include "attributes.h" #include "convert.h" #include "edgeobject.h" @@ -35,7 +37,9 @@ * \defgroup python_interface_vertex Vertex object */ -PyTypeObject igraphmodule_VertexType; +PyTypeObject* igraphmodule_VertexType; + +PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self); /** * \ingroup python_interface_vertex @@ -45,7 +49,7 @@ int igraphmodule_Vertex_Check(PyObject* obj) { if (!obj) return 0; - return PyObject_IsInstance(obj, (PyObject*)(&igraphmodule_VertexType)); + return PyObject_IsInstance(obj, (PyObject*)igraphmodule_VertexType); } /** @@ -101,13 +105,13 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { */ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { igraphmodule_VertexObject* self; - self=PyObject_New(igraphmodule_VertexObject, &igraphmodule_VertexType); + self = PyObject_New(igraphmodule_VertexObject, igraphmodule_VertexType); if (self) { RC_ALLOC("Vertex", self); Py_INCREF(gref); - self->gref=gref; - self->idx=idx; - self->hash=-1; + self->gref = gref; + self->idx = idx; + self->hash = -1; } return (PyObject*)self; } @@ -130,12 +134,15 @@ int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { * \ingroup python_interface_vertex * \brief Deallocates a Python representation of a given vertex object */ -void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { +static void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { + PyTypeObject* tp = Py_TYPE(self); + igraphmodule_Vertex_clear(self); RC_DEALLOC("Vertex", self); PyObject_Del((PyObject*)self); + Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ } /** \ingroup python_interface_vertex @@ -143,13 +150,14 @@ void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { * * \return the formatted textual representation as a \c PyObject */ -PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self) { +static PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self) { PyObject *s; PyObject *attrs; attrs = igraphmodule_Vertex_attributes(self); - if (attrs == 0) + if (attrs == 0) { return NULL; + } s = PyUnicode_FromFormat("igraph.Vertex(%R, %" IGRAPH_PRId ", %R)", (PyObject*)self->gref, self->idx, attrs); @@ -161,7 +169,7 @@ PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self) { /** \ingroup python_interface_vertex * \brief Returns the hash code of the vertex */ -long igraphmodule_Vertex_hash(igraphmodule_VertexObject* self) { +static long igraphmodule_Vertex_hash(igraphmodule_VertexObject* self) { long hash_graph; long hash_index; long result; @@ -198,7 +206,7 @@ long igraphmodule_Vertex_hash(igraphmodule_VertexObject* self) { /** \ingroup python_interface_vertex * \brief Rich comparison of a vertex with another */ -PyObject* igraphmodule_Vertex_richcompare(igraphmodule_VertexObject *a, +static PyObject* igraphmodule_Vertex_richcompare(igraphmodule_VertexObject *a, PyObject *b, int op) { igraphmodule_VertexObject* self = a; @@ -242,17 +250,18 @@ PyObject* igraphmodule_Vertex_richcompare(igraphmodule_VertexObject *a, Py_ssize_t igraphmodule_Vertex_attribute_count(igraphmodule_VertexObject* self) { igraphmodule_GraphObject *o = self->gref; - if (!o) return 0; - if (!((PyObject**)o->g.attr)[1]) return 0; - return PyDict_Size(((PyObject**)o->g.attr)[1]); + if (!o || !((PyObject**)o->g.attr)[1]) { + return 0; + } else { + return PyDict_Size(((PyObject**)o->g.attr)[1]); + } } /** \ingroup python_interface_vertex * \brief Returns the list of attribute names */ PyObject* igraphmodule_Vertex_attribute_names(igraphmodule_VertexObject* self) { - if (!self->gref) return NULL; - return igraphmodule_Graph_vertex_attributes(self->gref); + return self->gref ? igraphmodule_Graph_vertex_attributes(self->gref) : 0; } /** \ingroup python_interface_vertex @@ -263,8 +272,9 @@ PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self) { PyObject *names, *dict; Py_ssize_t i, n; - if (!igraphmodule_Vertex_Validate((PyObject*)self)) + if (!igraphmodule_Vertex_Validate((PyObject*)self)) { return 0; + } dict = PyDict_New(); if (!dict) { @@ -289,7 +299,15 @@ PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self) { /* No need to Py_INCREF, PyDict_SetItem will do that */ PyDict_SetItem(dict, name, value); } + } else { + Py_DECREF(dict); + Py_DECREF(names); + return NULL; } + } else { + Py_DECREF(dict); + Py_DECREF(names); + return NULL; } } @@ -601,10 +619,14 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje n = PyList_Size(obj); for (i = 0; i < n; i++) { - PyObject* idx = PyList_GET_ITEM(obj, i); - PyObject* v; + PyObject* idx = PyList_GetItem(obj, i); + PyObject* edge; igraph_integer_t idx_int; + if (!idx) { + return NULL; + } + if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_edge_list expected list of integers"); return NULL; @@ -614,15 +636,19 @@ static PyObject* _convert_to_edge_list(igraphmodule_VertexObject* vertex, PyObje return NULL; } - v = igraphmodule_Edge_New(vertex->gref, idx_int); - if (!v) { + edge = igraphmodule_Edge_New(vertex->gref, idx_int); + if (!edge) { return NULL; } - PyList_SetItem(obj, i, v); /* reference to v stolen, reference to idx discarded */ + if (PyList_SetItem(obj, i, edge)) { /* reference to v stolen, reference to idx discarded */ + Py_DECREF(edge); + return NULL; + } } Py_INCREF(obj); + return obj; } @@ -638,10 +664,14 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb n = PyList_Size(obj); for (i = 0; i < n; i++) { - PyObject* idx = PyList_GET_ITEM(obj, i); + PyObject* idx = PyList_GetItem(obj, i); PyObject* v; igraph_integer_t idx_int; + if (!idx) { + return NULL; + } + if (!PyLong_Check(idx)) { PyErr_SetString(PyExc_TypeError, "_convert_to_vertex_list expected list of integers"); return NULL; @@ -656,7 +686,10 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb return NULL; } - PyList_SetItem(obj, i, v); /* reference to v stolen, reference to idx discarded */ + if (PyList_SetItem(obj, i, v)) { /* reference to v stolen, reference to idx discarded */ + Py_DECREF(v); + return NULL; + } } Py_INCREF(obj); @@ -666,18 +699,25 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb #define GRAPH_PROXY_METHOD_PP(FUNC, METHODNAME, POSTPROCESS) \ PyObject* igraphmodule_Vertex_##FUNC(igraphmodule_VertexObject* self, PyObject* args, PyObject* kwds) { \ PyObject *new_args, *item, *result; \ - Py_ssize_t i, num_args = args ? PyTuple_Size(args)+1 : 1; \ + Py_ssize_t i, num_args = args ? PyTuple_Size(args) + 1 : 1; \ \ /* Prepend ourselves to args */ \ new_args = PyTuple_New(num_args); \ - Py_INCREF(self); PyTuple_SET_ITEM(new_args, 0, (PyObject*)self); \ + Py_INCREF(self); \ + PyTuple_SetItem(new_args, 0, (PyObject*)self); \ for (i = 1; i < num_args; i++) { \ - item = PyTuple_GET_ITEM(args, i-1); \ - Py_INCREF(item); PyTuple_SET_ITEM(new_args, i, item); \ + item = PyTuple_GetItem(args, i - 1); \ + Py_INCREF(item); \ + PyTuple_SetItem(new_args, i, item); \ } \ \ /* Get the method instance */ \ item = PyObject_GetAttrString((PyObject*)(self->gref), METHODNAME); \ + if (item == 0) { \ + Py_DECREF(new_args); \ + return 0; \ + } \ + \ result = PyObject_Call(item, new_args, kwds); \ Py_DECREF(item); \ Py_DECREF(new_args); \ @@ -688,6 +728,7 @@ static PyObject* _convert_to_vertex_list(igraphmodule_VertexObject* vertex, PyOb Py_DECREF(result); \ return pp_result; \ } \ + \ return NULL; \ } @@ -799,20 +840,6 @@ PyMethodDef igraphmodule_Vertex_methods[] = { #undef GRAPH_PROXY_METHOD_SPEC #undef GRAPH_PROXY_METHOD_SPEC_2 -/** \ingroup python_interface_vertex - * This structure is the collection of functions necessary to implement - * the vertex as a mapping (i.e. to allow the retrieval and setting of - * igraph attributes in Python as if it were of a Python mapping type) - */ -PyMappingMethods igraphmodule_Vertex_as_mapping = { - // returns the number of vertex attributes - (lenfunc)igraphmodule_Vertex_attribute_count, - // returns an attribute by name - (binaryfunc)igraphmodule_Vertex_get_attribute, - // sets an attribute by name - (objobjargproc)igraphmodule_Vertex_set_attribute -}; - /** * \ingroup python_interface_vertex * Getter/setter table for the \c igraph.Vertex object @@ -827,32 +854,8 @@ PyGetSetDef igraphmodule_Vertex_getseters[] = { {NULL} }; -/** \ingroup python_interface_vertex - * Python type object referencing the methods Python calls when it performs various operations on - * a vertex of a graph - */ -PyTypeObject igraphmodule_VertexType = -{ - PyVarObject_HEAD_INIT(0, 0) - "igraph.Vertex", /* tp_name */ - sizeof(igraphmodule_VertexObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)igraphmodule_Vertex_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - (reprfunc)igraphmodule_Vertex_repr, /* tp_repr */ - 0, /* tp_as_number */ - 0, /* tp_as_sequence */ - &igraphmodule_Vertex_as_mapping, /* tp_as_mapping */ - (hashfunc)igraphmodule_Vertex_hash, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ +PyDoc_STRVAR( + igraphmodule_Vertex_doc, "Class representing a single vertex in a graph.\n\n" "The vertex is referenced by its index, so if the underlying graph\n" "changes, the semantics of the vertex object might change as well\n" @@ -861,15 +864,34 @@ PyTypeObject igraphmodule_VertexType = "as a hash:\n\n" " >>> v[\"color\"] = \"red\" #doctest: +SKIP\n" " >>> print(v[\"color\"]) #doctest: +SKIP\n" - " red\n", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - (richcmpfunc)igraphmodule_Vertex_richcompare, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - igraphmodule_Vertex_methods, /* tp_methods */ - 0, /* tp_members */ - igraphmodule_Vertex_getseters, /* tp_getset */ -}; - + " red\n" +); + +int igraphmodule_Vertex_register_type() { + PyType_Slot slots[] = { + { Py_tp_dealloc, igraphmodule_Vertex_dealloc }, + { Py_tp_hash, igraphmodule_Vertex_hash }, + { Py_tp_repr, igraphmodule_Vertex_repr }, + { Py_tp_richcompare, igraphmodule_Vertex_richcompare }, + { Py_tp_methods, igraphmodule_Vertex_methods }, + { Py_tp_getset, igraphmodule_Vertex_getseters }, + { Py_tp_doc, (void*) igraphmodule_Vertex_doc }, + + { Py_mp_length, igraphmodule_Vertex_attribute_count }, + { Py_mp_subscript, igraphmodule_Vertex_get_attribute }, + { Py_mp_ass_subscript, igraphmodule_Vertex_set_attribute }, + + { 0 } + }; + + PyType_Spec spec = { + "igraph.Vertex", /* name */ + sizeof(igraphmodule_VertexObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ + slots, /* slots */ + }; + + igraphmodule_VertexType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_VertexType == 0; +} diff --git a/src/_igraph/vertexobject.h b/src/_igraph/vertexobject.h index d1667e3b9..e16e8c37c 100644 --- a/src/_igraph/vertexobject.h +++ b/src/_igraph/vertexobject.h @@ -39,20 +39,13 @@ typedef struct long hash; } igraphmodule_VertexObject; -int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self); -void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self); +extern PyTypeObject* igraphmodule_VertexType; -int igraphmodule_Vertex_Check(PyObject *obj); -int igraphmodule_Vertex_Validate(PyObject *obj); +int igraphmodule_Vertex_register_type(void); +int igraphmodule_Vertex_Check(PyObject* obj); PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx); -PyObject* igraphmodule_Vertex_repr(igraphmodule_VertexObject *self); -PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self); -PyObject* igraphmodule_Vertex_attribute_names(igraphmodule_VertexObject* self); igraph_integer_t igraphmodule_Vertex_get_index_igraph_integer(igraphmodule_VertexObject* self); -PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, - PyObject* kwds); - -extern PyTypeObject igraphmodule_VertexType; +PyObject* igraphmodule_Vertex_update_attributes(PyObject* self, PyObject* args, PyObject* kwds); #endif From c0165da6afacc484ae3b02994daf47c2e8aceae6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 16:21:03 +0200 Subject: [PATCH 0400/1681] fix: re-added missing tp_clear slots to Edge and Vertex --- src/_igraph/edgeobject.c | 3 ++- src/_igraph/vertexobject.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index b0ef72d3b..f0cbfb9d0 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -117,7 +117,7 @@ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t * \ingroup python_interface_edge * \brief Clears the edge's subobject (before deallocation) */ -int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { +static int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { PyObject *tmp; tmp=(PyObject*)self->gref; @@ -727,6 +727,7 @@ PyDoc_STRVAR( int igraphmodule_Edge_register_type() { PyType_Slot slots[] = { { Py_tp_dealloc, igraphmodule_Edge_dealloc }, + { Py_tp_clear, igraphmodule_Edge_clear }, { Py_tp_hash, igraphmodule_Edge_hash }, { Py_tp_repr, igraphmodule_Edge_repr }, { Py_tp_richcompare, igraphmodule_Edge_richcompare }, diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 699634789..0d146c39b 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -120,7 +120,7 @@ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer * \ingroup python_interface_vertex * \brief Clears the vertex's subobject (before deallocation) */ -int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { +static int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { PyObject *tmp; tmp=(PyObject*)self->gref; @@ -869,6 +869,7 @@ PyDoc_STRVAR( int igraphmodule_Vertex_register_type() { PyType_Slot slots[] = { + { Py_tp_clear, igraphmodule_Vertex_clear }, { Py_tp_dealloc, igraphmodule_Vertex_dealloc }, { Py_tp_hash, igraphmodule_Vertex_hash }, { Py_tp_repr, igraphmodule_Vertex_repr }, From 1f9b550481bfad559470ef4ae799c9d388018821 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 19:12:43 +0200 Subject: [PATCH 0401/1681] fix: fix a compiler warning about uninitialized variables --- src/_igraph/igraphmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 0fad4c359..947541c54 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -230,7 +230,7 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) { PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwds) { static char* kwlist[] = {"vs", "coords", NULL}; - PyObject *vs, *o, *o1, *o2, *o1_float, *o2_float, *coords = Py_False; + PyObject *vs, *o, *o1 = 0, *o2 = 0, *o1_float, *o2_float, *coords = Py_False; igraph_matrix_t mtrx; igraph_vector_int_t result; igraph_matrix_t resmat; From dbae48d6264a0321eded778327f70d151c31e2db Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 19:18:17 +0200 Subject: [PATCH 0402/1681] test: use pytest in all environments in CI --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e8c785feb..ed176ec2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -154,7 +154,7 @@ jobs: IGRAPH_STATIC_EXTENSION: True IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest -vvv" + CIBW_TEST_COMMAND: "cd /d {project} && python -m pytest tests" - uses: actions/upload-artifact@v2 with: @@ -196,8 +196,8 @@ jobs: - name: Test run: | - pip install numpy scipy pandas networkx - python -m unittest -vvv + pip install numpy scipy pandas networkx pytest + python -m pytest tests - uses: actions/upload-artifact@v2 with: From ea9c651edd2d9e63b79204bd6eeb0ece9db3958e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 19:20:37 +0200 Subject: [PATCH 0403/1681] test: try skipping Python 3.6 temporarily to see if the build fails with newer versions --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed176ec2b..62624ef20 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - CIBW_SKIP: "cp27-* pp27-* cp35-*" + CIBW_SKIP: "cp27-* pp27-* cp35-* cp36-*" jobs: build_wheels: From 1b52627bd6a13deb8d77a59a6124d10de0f74438 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 20:21:22 +0200 Subject: [PATCH 0404/1681] test: temporarily exclude Python 3.7 as well --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 62624ef20..9d4d7874f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - CIBW_SKIP: "cp27-* pp27-* cp35-* cp36-*" + CIBW_SKIP: "cp27-* pp27-* cp35-* cp36-* cp37-*" jobs: build_wheels: From 95077121135099bd73540dcb68f708f3864fc283 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 20:21:41 +0200 Subject: [PATCH 0405/1681] refactor: use Py_CLEAR macro --- src/_igraph/bfsiter.c | 6 ++---- src/_igraph/dfsiter.c | 9 +++------ src/_igraph/edgeobject.c | 7 +------ src/_igraph/vertexobject.c | 7 +------ 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 0f8dffd40..257cf5168 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -132,10 +132,8 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { PyObject *tmp; PyObject_GC_UnTrack(self); - - tmp = (PyObject*)self->gref; - self->gref = NULL; - Py_XDECREF(tmp); + + Py_CLEAR(self->gref); igraph_dqueue_int_destroy(&self->queue); igraph_vector_int_destroy(&self->neis); diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index d1e419d7c..9ff624cf7 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -130,16 +130,13 @@ static int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, * \brief Clears the iterator's subobject (before deallocation) */ static int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { - PyObject *tmp; - PyObject_GC_UnTrack(self); - - tmp = (PyObject*)self->gref; - self->gref = NULL; - Py_XDECREF(tmp); + + Py_CLEAR(self->gref); igraph_stack_int_destroy(&self->stack); igraph_vector_int_destroy(&self->neis); + free(self->visited); self->visited = 0; diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index f0cbfb9d0..1d09cda1d 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -118,12 +118,7 @@ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t * \brief Clears the edge's subobject (before deallocation) */ static int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { - PyObject *tmp; - - tmp=(PyObject*)self->gref; - self->gref=NULL; - Py_XDECREF(tmp); - + Py_CLEAR(self->gref); return 0; } diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 0d146c39b..bf6655f19 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -121,12 +121,7 @@ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer * \brief Clears the vertex's subobject (before deallocation) */ static int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { - PyObject *tmp; - - tmp=(PyObject*)self->gref; - self->gref=NULL; - Py_XDECREF(tmp); - + Py_CLEAR(self->gref); return 0; } From bc427c2ee4a2096b7f0260b5d99f7642d36fad77 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 21:24:39 +0200 Subject: [PATCH 0406/1681] test: re-add Python 3.6 and 3.7 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d4d7874f..ed176ec2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - CIBW_SKIP: "cp27-* pp27-* cp35-* cp36-* cp37-*" + CIBW_SKIP: "cp27-* pp27-* cp35-*" jobs: build_wheels: From 05c9edc5096e2fd32ac9ab7b57e2c03c1db99b65 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 10 Sep 2021 22:28:39 +0200 Subject: [PATCH 0407/1681] fix: fix reference counting errors with heap-allocated types --- .gitignore | 1 + src/_igraph/arpackobject.c | 1 + src/_igraph/bfsiter.c | 8 ++++++-- src/_igraph/dfsiter.c | 5 +++++ src/_igraph/edgeobject.c | 6 +++++- src/_igraph/vertexobject.c | 6 ++---- test.sh | 9 ++++++--- 7 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 323291f0c..2391ed900 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ src/igraph/*.so .eggs/ .tox .venv/ +.venv-*/ .vscode/ vendor/build/ vendor/install/ diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index c4e020450..f8523c289 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -36,6 +36,7 @@ PyObject* igraphmodule_ARPACKOptions_new() { igraphmodule_ARPACKOptionsObject* self; self = PyObject_New(igraphmodule_ARPACKOptionsObject, igraphmodule_ARPACKOptionsType); if (self) { + Py_INCREF(igraphmodule_ARPACKOptionsType); /* needed because heap-allocated types are refcounted */ igraph_arpack_options_init(&self->params); igraph_arpack_options_init(&self->params_out); } diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 257cf5168..3bab05eb1 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -48,6 +48,12 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_integer_t no_of_nodes, r; o = PyObject_GC_New(igraphmodule_BFSIterObject, igraphmodule_BFSIterType); + if (!o) { + return NULL; + } + + Py_INCREF(igraphmodule_BFSIterType); + Py_INCREF(g); o->gref = g; o->graph = &g->g; @@ -129,8 +135,6 @@ static int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, * \brief Clears the iterator's subobject (before deallocation) */ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { - PyObject *tmp; - PyObject_GC_UnTrack(self); Py_CLEAR(self->gref); diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 9ff624cf7..d9b32761d 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -48,6 +48,11 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_integer_t no_of_nodes, r; o = PyObject_GC_New(igraphmodule_DFSIterObject, igraphmodule_DFSIterType); + if (!o) { + return NULL; + } + Py_INCREF(igraphmodule_DFSIterType); + Py_INCREF(g); o->gref = g; o->graph = &g->g; diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 1d09cda1d..1e811c09f 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -46,7 +46,7 @@ PyObject* igraphmodule_Edge_attributes(igraphmodule_EdgeObject* self); * \brief Checks whether the given Python object is an edge */ int igraphmodule_Edge_Check(PyObject* obj) { - return obj ? PyObject_IsInstance(obj, (PyObject*)(igraphmodule_EdgeType)) : 0; + return obj ? PyObject_IsInstance(obj, (PyObject*)igraphmodule_EdgeType) : 0; } /** @@ -102,14 +102,18 @@ int igraphmodule_Edge_Validate(PyObject* obj) { */ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { igraphmodule_EdgeObject* self; + + self = PyObject_New(igraphmodule_EdgeObject, igraphmodule_EdgeType); if (self) { RC_ALLOC("Edge", self); + Py_INCREF(igraphmodule_EdgeType); Py_INCREF(gref); self->gref = gref; self->idx = idx; self->hash = -1; } + return (PyObject*)self; } diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index bf6655f19..eec5bdc6f 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -46,10 +46,7 @@ PyObject* igraphmodule_Vertex_attributes(igraphmodule_VertexObject* self); * \brief Checks whether the given Python object is a vertex */ int igraphmodule_Vertex_Check(PyObject* obj) { - if (!obj) - return 0; - - return PyObject_IsInstance(obj, (PyObject*)igraphmodule_VertexType); + return obj ? PyObject_IsInstance(obj, (PyObject*)igraphmodule_VertexType) : 0; } /** @@ -108,6 +105,7 @@ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer self = PyObject_New(igraphmodule_VertexObject, igraphmodule_VertexType); if (self) { RC_ALLOC("Vertex", self); + Py_INCREF(igraphmodule_VertexType); Py_INCREF(gref); self->gref = gref; self->idx = idx; diff --git a/test.sh b/test.sh index d5c4193cb..64d9b8789 100755 --- a/test.sh +++ b/test.sh @@ -1,6 +1,5 @@ #!/bin/bash -VENV_DIR=.venv PYTHON=python3 ############################################################################### @@ -8,18 +7,22 @@ PYTHON=python3 set -e CLEAN=0 +VENV_DIR=.venv -while getopts ":c" OPTION; do +while getopts ":ce:" OPTION; do case $OPTION in c) CLEAN=1 ;; + e) + VENV_DIR=$OPTARG + ;; \?) echo "Usage: $0 [-c]" ;; esac + shift $((OPTIND -1)) done -shift $((OPTIND -1)) if [ ! -d $VENV_DIR ]; then $PYTHON -m venv $VENV_DIR From c4a3731fcf5c1dda0a3cfbd0c397287c87665dea Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 12 Sep 2021 00:45:16 +0200 Subject: [PATCH 0408/1681] feat: EdgeSeq and VertexSeq are heap-allocated --- src/_igraph/arpackobject.c | 28 +-- src/_igraph/arpackobject.h | 3 +- src/_igraph/attributes.c | 9 +- src/_igraph/bfsiter.c | 58 +++--- src/_igraph/convert.c | 58 +++--- src/_igraph/dfsiter.c | 61 +++--- src/_igraph/edgeobject.c | 14 +- src/_igraph/edgeseqobject.c | 346 +++++++++++++++++++--------------- src/_igraph/edgeseqobject.h | 21 +-- src/_igraph/graphobject.c | 58 +----- src/_igraph/graphobject.h | 1 - src/_igraph/igraphmodule.c | 19 +- src/_igraph/pyhelpers.h | 15 ++ src/_igraph/vertexobject.c | 17 +- src/_igraph/vertexseqobject.c | 330 ++++++++++++++++++-------------- src/_igraph/vertexseqobject.h | 21 +-- test.sh | 11 +- 17 files changed, 540 insertions(+), 530 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index f8523c289..86d07f307 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -22,8 +22,9 @@ #include "arpackobject.h" #include "convert.h" -#include "graphobject.h" #include "error.h" +#include "graphobject.h" +#include "pyhelpers.h" PyTypeObject* igraphmodule_ARPACKOptionsType; PyObject* igraphmodule_arpack_options_default; @@ -32,15 +33,17 @@ PyObject* igraphmodule_arpack_options_default; * \ingroup python_interface_arpack * \brief Allocates a new ARPACK parameters object */ -PyObject* igraphmodule_ARPACKOptions_new() { - igraphmodule_ARPACKOptionsObject* self; - self = PyObject_New(igraphmodule_ARPACKOptionsObject, igraphmodule_ARPACKOptionsType); - if (self) { - Py_INCREF(igraphmodule_ARPACKOptionsType); /* needed because heap-allocated types are refcounted */ - igraph_arpack_options_init(&self->params); - igraph_arpack_options_init(&self->params_out); +int igraphmodule_ARPACKOptions_init(igraphmodule_ARPACKOptionsObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { NULL }; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "", kwlist)) { + return -1; } - return (PyObject*)self; + + igraph_arpack_options_init(&self->params); + igraph_arpack_options_init(&self->params_out); + + return 0; } /** @@ -48,9 +51,8 @@ PyObject* igraphmodule_ARPACKOptions_new() { * \brief Deallocates a Python representation of a given ARPACK parameters object */ static void igraphmodule_ARPACKOptions_dealloc(igraphmodule_ARPACKOptionsObject* self) { - PyTypeObject *tp = Py_TYPE(self); - PyObject_Del((PyObject*)self); - Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ + RC_DEALLOC("ARPACKOptions", self); + PY_FREE_AND_DECREF_TYPE(self); } /** \ingroup python_interface_arpack @@ -211,7 +213,7 @@ PyDoc_STRVAR( int igraphmodule_ARPACKOptions_register_type() { PyType_Slot slots[] = { - { Py_tp_new, igraphmodule_ARPACKOptions_new }, + { Py_tp_init, igraphmodule_ARPACKOptions_init }, { Py_tp_dealloc, igraphmodule_ARPACKOptions_dealloc }, { Py_tp_getattr, igraphmodule_ARPACKOptions_getattr }, { Py_tp_setattr, igraphmodule_ARPACKOptions_setattr }, diff --git a/src/_igraph/arpackobject.h b/src/_igraph/arpackobject.h index ea482444b..affc94718 100644 --- a/src/_igraph/arpackobject.h +++ b/src/_igraph/arpackobject.h @@ -23,7 +23,7 @@ #ifndef PYTHON_ARPACKOBJECT_H #define PYTHON_ARPACKOBJECT_H -#define Py_LIMITED_API 0x03060000 +#define Py_LIMITED_API 0x03060100 #include "preamble.h" #include @@ -49,7 +49,6 @@ extern PyObject* igraphmodule_arpack_options_default; int igraphmodule_ARPACKOptions_register_type(void); -PyObject* igraphmodule_ARPACKOptions_new(void); igraph_arpack_options_t *igraphmodule_ARPACKOptions_get(igraphmodule_ARPACKOptionsObject *self); #endif diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 765f902f2..86400f521 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -20,7 +20,7 @@ */ -#define Py_LIMITED_API 0x03060000 +#define Py_LIMITED_API 0x03060100 #include "attributes.h" #include "common.h" @@ -2025,12 +2025,13 @@ void igraphmodule_initialize_attribute_handler(void) { * Also raises a suitable Python exception if needed. */ int igraphmodule_attribute_name_check(PyObject* obj) { - PyObject* type_obj; + PyTypeObject* type_obj; - if (obj != 0 && PyBaseString_Check(obj)) + if (obj != 0 && PyBaseString_Check(obj)) { return 1; + } - type_obj = obj ? ((PyObject*)obj->ob_type) : 0; + type_obj = Py_TYPE(obj); if (type_obj != 0) { PyErr_Format(PyExc_TypeError, "igraph supports string attribute names only, got %R", type_obj); } else { diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 3bab05eb1..bb26e23e1 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -20,12 +20,13 @@ */ -#define Py_LIMITED_API 0x03060000 +#define Py_LIMITED_API 0x03060100 #include "bfsiter.h" #include "common.h" #include "convert.h" #include "error.h" +#include "pyhelpers.h" #include "vertexobject.h" /** @@ -44,19 +45,17 @@ PyTypeObject* igraphmodule_BFSIterType; * \return the allocated PyObject */ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { - igraphmodule_BFSIterObject* o; + igraphmodule_BFSIterObject* self; igraph_integer_t no_of_nodes, r; - o = PyObject_GC_New(igraphmodule_BFSIterObject, igraphmodule_BFSIterType); - if (!o) { + self = (igraphmodule_BFSIterObject*) PyType_GenericNew(igraphmodule_BFSIterType, 0, 0); + if (!self) { return NULL; } - - Py_INCREF(igraphmodule_BFSIterType); Py_INCREF(g); - o->gref = g; - o->graph = &g->g; + self->gref = g; + self->graph = &g->g; if (!PyLong_Check(root) && !igraphmodule_Vertex_Check(root)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); @@ -64,55 +63,53 @@ PyObject* igraphmodule_BFSIter_new(igraphmodule_GraphObject *g, PyObject *root, } no_of_nodes = igraph_vcount(&g->g); - o->visited = (char*)calloc(no_of_nodes, sizeof(char)); - if (o->visited == 0) { + self->visited = (char*)calloc(no_of_nodes, sizeof(char)); + if (self->visited == 0) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_dqueue_int_init(&o->queue, 100)) { + if (igraph_dqueue_int_init(&self->queue, 100)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_vector_int_init(&o->neis, 0)) { + if (igraph_vector_int_init(&self->neis, 0)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); - igraph_dqueue_int_destroy(&o->queue); + igraph_dqueue_int_destroy(&self->queue); return NULL; } if (PyLong_Check(root)) { if (igraphmodule_PyObject_to_integer_t(root, &r)) { - igraph_dqueue_int_destroy(&o->queue); - igraph_vector_int_destroy(&o->neis); + igraph_dqueue_int_destroy(&self->queue); + igraph_vector_int_destroy(&self->neis); return NULL; } } else { r = ((igraphmodule_VertexObject*)root)->idx; } - if (igraph_dqueue_int_push(&o->queue, r) || - igraph_dqueue_int_push(&o->queue, 0) || - igraph_dqueue_int_push(&o->queue, -1)) { - igraph_dqueue_int_destroy(&o->queue); - igraph_vector_int_destroy(&o->neis); + if (igraph_dqueue_int_push(&self->queue, r) || + igraph_dqueue_int_push(&self->queue, 0) || + igraph_dqueue_int_push(&self->queue, -1)) { + igraph_dqueue_int_destroy(&self->queue); + igraph_vector_int_destroy(&self->neis); PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - o->visited[r] = 1; + self->visited[r] = 1; if (!igraph_is_directed(&g->g)) { mode=IGRAPH_ALL; } - o->mode = mode; - o->advanced = advanced; - - PyObject_GC_Track(o); + self->mode = mode; + self->advanced = advanced; - RC_ALLOC("BFSIter", o); + RC_ALLOC("BFSIter", self); - return (PyObject*)o; + return (PyObject*)self; } /** @@ -153,14 +150,11 @@ int igraphmodule_BFSIter_clear(igraphmodule_BFSIterObject *self) { * \brief Deallocates a Python representation of a given BFS iterator object */ static void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { - PyTypeObject *tp = Py_TYPE(self); + RC_DEALLOC("BFSIter", self); igraphmodule_BFSIter_clear(self); - RC_DEALLOC("BFSIter", self); - - PyObject_GC_Del(self); - Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ + PY_FREE_AND_DECREF_TYPE(self); } static PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 2d9234328..762d60366 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2764,7 +2764,7 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g /* Single vertex ID from Vertex object */ igraphmodule_VertexObject *vo = (igraphmodule_VertexObject*)o; *vid = igraphmodule_Vertex_get_index_igraph_integer(vo); - } else if (PyIndex_Check(o)) { + } else { /* Other numeric type that can be converted to an index */ PyObject* num = PyNumber_Index(o); if (num) { @@ -2774,17 +2774,15 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g return 1; } } else { - PyErr_SetString(PyExc_TypeError, "PyNumber_Index returned invalid type"); + PyErr_SetString(PyExc_TypeError, "PyNumber_Index() returned invalid type"); Py_DECREF(num); return 1; } Py_DECREF(num); } else { + PyErr_SetString(PyExc_TypeError, "only numbers, strings or igraph.Vertex objects can be converted to vertex IDs"); return 1; } - } else { - PyErr_SetString(PyExc_TypeError, "only numbers, strings or igraph.Vertex objects can be converted to vertex IDs"); - return 1; } if (*vid < 0) { @@ -2823,7 +2821,7 @@ int igraphmodule_PyObject_to_vs_t(PyObject *o, igraph_vs_t *vs, return 0; } - if (PyObject_IsInstance(o, (PyObject*)&igraphmodule_VertexSeqType)) { + if (igraphmodule_VertexSeq_Check(o)) { /* Returns a vertex sequence from a VertexSeq object */ igraphmodule_VertexSeqObject *vso = (igraphmodule_VertexSeqObject*)o; @@ -2975,24 +2973,6 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g /* Single edge ID from Edge object */ igraphmodule_EdgeObject *eo = (igraphmodule_EdgeObject*)o; *eid = igraphmodule_Edge_get_index_as_igraph_integer(eo); - } else if (PyIndex_Check(o)) { - /* Other numeric type that can be converted to an index */ - PyObject* num = PyNumber_Index(o); - if (num) { - if (PyLong_Check(num)) { - if (igraphmodule_PyObject_to_integer_t(num, eid)) { - Py_DECREF(num); - return 1; - } - } else { - PyErr_SetString(PyExc_TypeError, "PyNumber_Index returned invalid type"); - Py_DECREF(num); - return 1; - } - Py_DECREF(num); - } else { - return 1; - } } else if (graph != 0 && PyTuple_Check(o)) { PyObject *o1, *o2; @@ -3001,8 +2981,8 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g return 1; } - o2 = PyTuple_GetItem(o, 1); { - if (!o2) + o2 = PyTuple_GetItem(o, 1); + if (!o2) { return 1; } @@ -3035,10 +3015,26 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g return 1; } } else { - PyErr_SetString(PyExc_TypeError, - "only numbers, igraph.Edge objects or tuples of vertex IDs can be " - "converted to edge IDs"); - return 1; + /* Other numeric type that can be converted to an index */ + PyObject* num = PyNumber_Index(o); + if (num) { + if (PyLong_Check(num)) { + if (igraphmodule_PyObject_to_integer_t(num, eid)) { + Py_DECREF(num); + return 1; + } + } else { + PyErr_SetString(PyExc_TypeError, "PyNumber_Index() returned invalid type"); + Py_DECREF(num); + return 1; + } + Py_DECREF(num); + } else { + PyErr_SetString(PyExc_TypeError, + "only numbers, igraph.Edge objects or tuples of vertex IDs can be " + "converted to edge IDs"); + return 1; + } } if (*eid < 0) { @@ -3075,7 +3071,7 @@ int igraphmodule_PyObject_to_es_t(PyObject *o, igraph_es_t *es, igraph_t *graph, return 0; } - if (PyObject_IsInstance(o, (PyObject*)&igraphmodule_EdgeSeqType)) { + if (igraphmodule_EdgeSeq_Check(o)) { /* Returns an edge sequence from an EdgeSeq object */ igraphmodule_EdgeSeqObject *eso = (igraphmodule_EdgeSeqObject*)o; if (igraph_es_copy(es, &eso->es)) { diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index d9b32761d..4f0bcdbc9 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -20,12 +20,13 @@ */ -#define Py_LIMITED_API 0x03060000 +#define Py_LIMITED_API 0x03060100 #include "convert.h" -#include "dfsiter.h" #include "common.h" +#include "dfsiter.h" #include "error.h" +#include "pyhelpers.h" #include "vertexobject.h" /** @@ -44,18 +45,17 @@ PyTypeObject* igraphmodule_DFSIterType; * \return the allocated PyObject */ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, igraph_neimode_t mode, igraph_bool_t advanced) { - igraphmodule_DFSIterObject* o; + igraphmodule_DFSIterObject* self; igraph_integer_t no_of_nodes, r; - o = PyObject_GC_New(igraphmodule_DFSIterObject, igraphmodule_DFSIterType); - if (!o) { + self = (igraphmodule_DFSIterObject*) PyType_GenericNew(igraphmodule_DFSIterType, 0, 0); + if (!self) { return NULL; } - Py_INCREF(igraphmodule_DFSIterType); Py_INCREF(g); - o->gref = g; - o->graph = &g->g; + self->gref = g; + self->graph = &g->g; if (!PyLong_Check(root) && !igraphmodule_Vertex_Check(root)) { PyErr_SetString(PyExc_TypeError, "root must be integer or igraph.Vertex"); @@ -63,27 +63,27 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, } no_of_nodes = igraph_vcount(&g->g); - o->visited = (char*)calloc(no_of_nodes, sizeof(char)); - if (o->visited == 0) { + self->visited = (char*)calloc(no_of_nodes, sizeof(char)); + if (self->visited == 0) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_stack_int_init(&o->stack, 100)) { + if (igraph_stack_int_init(&self->stack, 100)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - if (igraph_vector_int_init(&o->neis, 0)) { + if (igraph_vector_int_init(&self->neis, 0)) { PyErr_SetString(PyExc_MemoryError, "out of memory"); - igraph_stack_int_destroy(&o->stack); + igraph_stack_int_destroy(&self->stack); return NULL; } if (PyLong_Check(root)) { if (igraphmodule_PyObject_to_integer_t(root, &r)) { - igraph_stack_int_destroy(&o->stack); - igraph_vector_int_destroy(&o->neis); + igraph_stack_int_destroy(&self->stack); + igraph_vector_int_destroy(&self->neis); return NULL; } } else { @@ -91,28 +91,26 @@ PyObject* igraphmodule_DFSIter_new(igraphmodule_GraphObject *g, PyObject *root, } /* push the root onto the stack */ - if (igraph_stack_int_push(&o->stack, r) || - igraph_stack_int_push(&o->stack, 0) || - igraph_stack_int_push(&o->stack, -1)) { - igraph_stack_int_destroy(&o->stack); - igraph_vector_int_destroy(&o->neis); + if (igraph_stack_int_push(&self->stack, r) || + igraph_stack_int_push(&self->stack, 0) || + igraph_stack_int_push(&self->stack, -1)) { + igraph_stack_int_destroy(&self->stack); + igraph_vector_int_destroy(&self->neis); PyErr_SetString(PyExc_MemoryError, "out of memory"); return NULL; } - o->visited[r] = 1; + self->visited[r] = 1; if (!igraph_is_directed(&g->g)) { mode = IGRAPH_ALL; } - o->mode = mode; - o->advanced = advanced; - - PyObject_GC_Track(o); + self->mode = mode; + self->advanced = advanced; - RC_ALLOC("DFSIter", o); + RC_ALLOC("DFSIter", self); - return (PyObject*)o; + return (PyObject*)self; } /** @@ -153,14 +151,9 @@ static int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { * \brief Deallocates a Python representation of a given DFS iterator object */ static void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { - PyTypeObject *tp = Py_TYPE(self); - - igraphmodule_DFSIter_clear(self); - RC_DEALLOC("DFSIter", self); - - PyObject_GC_Del(self); - Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ + igraphmodule_DFSIter_clear(self); + PY_FREE_AND_DECREF_TYPE(self); } static PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 1e811c09f..5120456a5 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -22,7 +22,7 @@ */ -#define Py_LIMITED_API 0x03060000 +#define Py_LIMITED_API 0x03060100 #include "attributes.h" #include "convert.h" @@ -103,11 +103,10 @@ int igraphmodule_Edge_Validate(PyObject* obj) { PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { igraphmodule_EdgeObject* self; + self = (igraphmodule_EdgeObject*) PyType_GenericNew(igraphmodule_EdgeType, 0, 0); - self = PyObject_New(igraphmodule_EdgeObject, igraphmodule_EdgeType); if (self) { RC_ALLOC("Edge", self); - Py_INCREF(igraphmodule_EdgeType); Py_INCREF(gref); self->gref = gref; self->idx = idx; @@ -131,14 +130,9 @@ static int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { * \brief Deallocates a Python representation of a given edge object */ static void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { - PyTypeObject* tp = Py_TYPE(self); - - igraphmodule_Edge_clear(self); - RC_DEALLOC("Edge", self); - - PyObject_Del((PyObject*)self); - Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ + igraphmodule_Edge_clear(self); + PY_FREE_AND_DECREF_TYPE(self); } /** \ingroup python_interface_edge diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 86de684ed..fbb9c6703 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -22,6 +22,8 @@ */ +#define Py_LIMITED_API 0x03060100 + #include "attributes.h" #include "common.h" #include "convert.h" @@ -37,28 +39,16 @@ * \defgroup python_interface_edgeseq Edge sequence object */ -PyTypeObject igraphmodule_EdgeSeqType; +PyTypeObject* igraphmodule_EdgeSeqType; + +PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject *args); /** * \ingroup python_interface_edgeseq - * \brief Allocate a new edge sequence object for a given graph - * \param g the graph object being referenced - * \return the allocated PyObject + * \brief Checks whether the given Python object is an edge sequence */ -PyObject* igraphmodule_EdgeSeq_new(PyTypeObject *subtype, - PyObject *args, PyObject *kwds) { - igraphmodule_EdgeSeqObject* o; - - o=(igraphmodule_EdgeSeqObject*)PyType_GenericNew(subtype, args, kwds); - if (o == NULL) return NULL; - - igraph_es_all(&o->es, IGRAPH_EDGEORDER_ID); - o->gref=0; - o->weakreflist=0; - - RC_ALLOC("EdgeSeq", o); - - return (PyObject*)o; +int igraphmodule_EdgeSeq_Check(PyObject* obj) { + return obj ? PyObject_IsInstance(obj, (PyObject*)igraphmodule_EdgeSeqType) : 0; } /** @@ -70,8 +60,10 @@ igraphmodule_EdgeSeqObject* igraphmodule_EdgeSeq_copy(igraphmodule_EdgeSeqObject* o) { igraphmodule_EdgeSeqObject *copy; - copy=(igraphmodule_EdgeSeqObject*)PyType_GenericNew(Py_TYPE(o), 0, 0); - if (copy == NULL) return NULL; + copy = (igraphmodule_EdgeSeqObject*) PyType_GenericNew(Py_TYPE(o), 0, 0); + if (copy == NULL) { + return NULL; + } if (igraph_es_type(&o->es) == IGRAPH_ES_VECTOR) { igraph_vector_int_t v; @@ -90,7 +82,10 @@ igraphmodule_EdgeSeq_copy(igraphmodule_EdgeSeqObject* o) { } copy->gref = o->gref; - if (o->gref) Py_INCREF(o->gref); + if (o->gref) { + Py_INCREF(o->gref); + } + RC_ALLOC("EdgeSeq(copy)", copy); return copy; @@ -102,15 +97,15 @@ igraphmodule_EdgeSeq_copy(igraphmodule_EdgeSeqObject* o) { * \brief Initialize a new edge sequence object for a given graph * \return the initialized PyObject */ -int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, - PyObject *args, PyObject *kwds) { +int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, PyObject *kwds) { static char *kwlist[] = { "graph", "edges", NULL }; - PyObject *g, *esobj=Py_None; + PyObject *g, *esobj = Py_None; igraph_es_t es; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, - &igraphmodule_GraphType, &g, &esobj)) - return -1; + &igraphmodule_GraphType, &g, &esobj)) { + return -1; + } if (esobj == Py_None) { /* If es is None, we are selecting all the edges */ @@ -130,11 +125,12 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, igraph_es_1(&es, idx); } else { - /* We selected multiple edges */ + /* We selected multiple edges */ igraph_vector_int_t v; igraph_integer_t n = igraph_ecount(&((igraphmodule_GraphObject*)g)->g); - if (igraphmodule_PyObject_to_vector_int_t(esobj, &v)) + if (igraphmodule_PyObject_to_vector_int_t(esobj, &v)) { return -1; + } if (!igraph_vector_int_isininterval(&v, 0, n-1)) { igraph_vector_int_destroy(&v); PyErr_SetString(PyExc_ValueError, "edge index out of range"); @@ -160,16 +156,19 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, * \brief Deallocates a Python representation of a given edge sequence object */ void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self) { - if (self->weakreflist != NULL) + RC_DEALLOC("EdgeSeq", self); + + if (self->weakreflist != NULL) { PyObject_ClearWeakRefs((PyObject *)self); + } + if (self->gref) { igraph_es_destroy(&self->es); Py_DECREF(self->gref); self->gref=0; } - Py_TYPE(self)->tp_free((PyObject*)self); - RC_DEALLOC("EdgeSeq", self); + PY_FREE_AND_DECREF_TYPE(self); } /** @@ -180,11 +179,13 @@ Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject* self) { igraph_t *g; igraph_integer_t result; - g=&GET_GRAPH(self); + g = &GET_GRAPH(self); + if (igraph_es_size(g, &self->es, &result)) { igraphmodule_handle_igraph_error(); return -1; } + return result; } @@ -197,8 +198,12 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, igraph_t *g; igraph_integer_t idx = -1; - if (!self->gref) return NULL; - g=&GET_GRAPH(self); + if (!self->gref) { + return NULL; + } + + g = &GET_GRAPH(self); + switch (igraph_es_type(&self->es)) { case IGRAPH_ES_ALL: if (i < 0) { @@ -246,6 +251,7 @@ PyObject* igraphmodule_EdgeSeq_sq_item(igraphmodule_EdgeSeqObject* self, igraphmodule_InternalError, "unsupported edge selector type: %d", igraph_es_type(&self->es) ); } + if (idx < 0) { PyErr_SetString(PyExc_IndexError, "edge index out of range"); return NULL; @@ -269,8 +275,9 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* PyObject *result=0, *values, *item; Py_ssize_t i, n; - if (!igraphmodule_attribute_name_check(o)) + if (!igraphmodule_attribute_name_check(o)) { return 0; + } PyErr_Clear(); values = PyDict_GetItem(ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE], o); @@ -298,9 +305,19 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* } for (i = 0; i < n; i++) { - item = PyList_GET_ITEM(values, i); + item = PyList_GetItem(values, i); + if (!item) { + Py_DECREF(result); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; @@ -313,9 +330,19 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* } for (i = 0; i < n; i++) { - item = PyList_GET_ITEM(values, VECTOR(*self->es.data.vecptr)[i]); + item = PyList_GetItem(values, VECTOR(*self->es.data.vecptr)[i]); + if (!item) { + Py_DECREF(result); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; @@ -327,9 +354,19 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* } for (i = 0; i < n; i++) { - item = PyList_GET_ITEM(values, self->es.data.seq.from + i); + item = PyList_GetItem(values, self->es.data.seq.from + i); + if (!item) { + Py_DECREF(result); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; @@ -341,35 +378,53 @@ PyObject* igraphmodule_EdgeSeq_get_attribute_values(igraphmodule_EdgeSeqObject* } PyObject* igraphmodule_EdgeSeq_is_all(igraphmodule_EdgeSeqObject* self) { - if (igraph_es_is_all(&self->es)) + if (igraph_es_is_all(&self->es)) { Py_RETURN_TRUE; - Py_RETURN_FALSE; + } else { + Py_RETURN_FALSE; + } } PyObject* igraphmodule_EdgeSeq_get_attribute_values_mapping(igraphmodule_EdgeSeqObject *self, PyObject *o) { Py_ssize_t index; - - /* Handle integer indices according to the sequence protocol */ - if (PyIndex_Check(o)) { - index = PyNumber_AsSsize_t(o, 0); - return igraphmodule_EdgeSeq_sq_item(self, index); - } + PyObject *index_o; /* Handle strings according to the mapping protocol */ - if (PyBaseString_Check(o)) + if (PyBaseString_Check(o)) { return igraphmodule_EdgeSeq_get_attribute_values(self, o); + } /* Handle iterables and slices by calling the select() method */ if (PySlice_Check(o) || PyObject_HasAttrString(o, "__iter__")) { PyObject *result, *args; - args = Py_BuildValue("(O)", o); - if (!args) + args = PyTuple_Pack(1, o); + + if (!args) { return NULL; + } + result = igraphmodule_EdgeSeq_select(self, args); Py_DECREF(args); + return result; } + /* Handle integer indices according to the sequence protocol */ + index_o = PyNumber_Index(o); + if (index_o) { + index = PyLong_AsSsize_t(index_o); + if (PyErr_Occurred()) { + Py_DECREF(index_o); + return NULL; + } else { + Py_DECREF(index_o); + return igraphmodule_EdgeSeq_sq_item(self, index); + } + } else { + /* clear TypeError raised by PyNumber_Index() */ + PyErr_Clear(); + } + /* Handle everything else according to the mapping protocol */ return igraphmodule_EdgeSeq_get_attribute_values(self, o); } @@ -387,12 +442,14 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject gr = self->gref; dict = ATTR_STRUCT_DICT(&gr->g)[ATTRHASH_IDX_EDGE]; - if (!igraphmodule_attribute_name_check(attrname)) + if (!igraphmodule_attribute_name_check(attrname)) { return -1; + } if (values == 0) { - if (igraph_es_type(&self->es) == IGRAPH_ES_ALL) + if (igraph_es_type(&self->es) == IGRAPH_ES_ALL) { return PyDict_DelItem(dict, attrname); + } PyErr_SetString(PyExc_TypeError, "can't delete attribute from an edge sequence not representing the whole graph"); return -1; } @@ -406,7 +463,11 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject return -1; } Py_INCREF(values); - PyList_SET_ITEM(newList, 0, values); /* reference stolen here */ + + if (PyList_SetItem(newList, 0, values)) { /* reference stolen here */ + return -1; + } + result = igraphmodule_EdgeSeq_set_attribute_values_mapping(self, attrname, newList); Py_DECREF(newList); return result; @@ -458,7 +519,11 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject Py_DECREF(list); return -1; } /* No need to Py_INCREF(item), PySequence_GetItem returns a new reference */ - PyList_SET_ITEM(list, i, item); + if (PyList_SetItem(list, i, item)) { + Py_DECREF(item); + Py_DECREF(list); + return -1; + } /* PyList_SET_ITEM stole a reference to the item automatically */ } if (PyDict_SetItem(dict, attrname, list)) { @@ -512,11 +577,16 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject igraph_integer_t n2 = igraph_ecount(&gr->g); list = PyList_New(n2); if (list == 0) { - igraph_vector_int_destroy(&es); return -1; + igraph_vector_int_destroy(&es); + return -1; } for (i = 0; i < n2; i++) { Py_INCREF(Py_None); - PyList_SET_ITEM(list, i, Py_None); + if (PyList_SetItem(list, i, Py_None)) { + Py_DECREF(Py_None); + Py_DECREF(list); + return -1; + } } for (i = 0, j = 0; i < no_of_edges; i++, j++) { if (j == n) { @@ -525,10 +595,15 @@ int igraphmodule_EdgeSeq_set_attribute_values_mapping(igraphmodule_EdgeSeqObject item = PySequence_GetItem(values, j); if (item == 0) { igraph_vector_int_destroy(&es); - Py_DECREF(list); return -1; + Py_DECREF(list); + return -1; } /* No need to Py_INCREF(item), PySequence_GetItem returns a new reference */ - PyList_SET_ITEM(list, VECTOR(es)[i], item); + if (PyList_SetItem(list, VECTOR(es)[i], item)) { + Py_DECREF(item); + Py_DECREF(list); + return -1; + } /* PyList_SET_ITEM stole a reference to the item automatically */ } igraph_vector_int_destroy(&es); @@ -614,16 +689,20 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject igraph_integer_t igraph_idx; Py_ssize_t i, j, n, m; - gr=self->gref; - result=igraphmodule_EdgeSeq_copy(self); - if (result == 0) + gr = self->gref; + result = igraphmodule_EdgeSeq_copy(self); + if (result == 0) { return NULL; + } /* First, filter by positional arguments */ n = PyTuple_Size(args); for (i = 0; i < n; i++) { - PyObject *item = PyTuple_GET_ITEM(args, i); - if (item == Py_None) { + PyObject *item = PyTuple_GetItem(args, i); + if (item == 0) { + Py_DECREF(result); + return NULL; + } else if (item == Py_None) { /* None means: select nothing */ igraph_es_destroy(&result->es); igraph_es_none(&result->es); @@ -698,8 +777,15 @@ PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, PyObject } m = igraph_vector_int_size(&v2); for (; i= m || idx < 0) { @@ -886,39 +974,6 @@ PyMethodDef igraphmodule_EdgeSeq_methods[] = { {NULL} }; -/** - * \ingroup python_interface_edgeseq - * This is the collection of functions necessary to implement the - * edge sequence as a real sequence (e.g. allowing to reference - * edges by indices) - */ -static PySequenceMethods igraphmodule_EdgeSeq_as_sequence = { - (lenfunc)igraphmodule_EdgeSeq_sq_length, - 0, /* sq_concat */ - 0, /* sq_repeat */ - (ssizeargfunc)igraphmodule_EdgeSeq_sq_item, /* sq_item */ - 0, /* sq_slice */ - 0, /* sq_ass_item */ - 0, /* sq_ass_slice */ - 0, /* sq_contains */ - 0, /* sq_inplace_concat */ - 0, /* sq_inplace_repeat */ -}; - -/** - * \ingroup python_interface_edgeseq - * This is the collection of functions necessary to implement the - * edge sequence as a mapping (which maps attribute names to values) - */ -static PyMappingMethods igraphmodule_EdgeSeq_as_mapping = { - /* returns the number of edge attributes */ - (lenfunc) 0, - /* returns the values of an attribute by name */ - (binaryfunc) igraphmodule_EdgeSeq_get_attribute_values_mapping, - /* sets the values of an attribute by name */ - (objobjargproc) igraphmodule_EdgeSeq_set_attribute_values_mapping, -}; - /** * \ingroup python_interface_edgeseq * Returns the graph where the edge sequence belongs @@ -968,57 +1023,48 @@ PyGetSetDef igraphmodule_EdgeSeq_getseters[] = { {NULL} }; -/** \ingroup python_interface_edgeseq - * Python type object referencing the methods Python calls when it performs various operations on - * an edge sequence of a graph +/** + * \ingroup python_interface_edgeseq + * Member table for the \c igraph.EdgeSeq object */ -PyTypeObject igraphmodule_EdgeSeqType = -{ - PyVarObject_HEAD_INIT(0, 0) - "igraph._igraph.EdgeSeq", /* tp_name */ - sizeof(igraphmodule_EdgeSeqObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)igraphmodule_EdgeSeq_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - &igraphmodule_EdgeSeq_as_sequence, /* tp_as_sequence */ - &igraphmodule_EdgeSeq_as_mapping, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ +PyMemberDef igraphmodule_EdgeSeq_members[] = { + {"__weaklistoffset__", T_PYSSIZET, offsetof(igraphmodule_EdgeSeqObject, weakreflist), READONLY}, + { 0 } +}; + +PyDoc_STRVAR( + igraphmodule_EdgeSeq_doc, "Low-level representation of an edge sequence.\n\n" /* tp_doc */ "Don't use it directly, use L{igraph.EdgeSeq} instead.\n\n" - "@deffield ref: Reference", - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - offsetof(igraphmodule_EdgeSeqObject, weakreflist), /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - igraphmodule_EdgeSeq_methods, /* tp_methods */ - 0, /* tp_members */ - igraphmodule_EdgeSeq_getseters, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc) igraphmodule_EdgeSeq_init, /* tp_init */ - 0, /* tp_alloc */ - (newfunc) igraphmodule_EdgeSeq_new, /* tp_new */ - 0, /* tp_free */ - 0, /* tp_is_gc */ - 0, /* tp_bases */ - 0, /* tp_mro */ - 0, /* tp_cache */ - 0, /* tp_subclasses */ - 0, /* tp_weakreflist */ -}; + "@deffield ref: Reference" +); + +int igraphmodule_EdgeSeq_register_type() { + PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_EdgeSeq_init }, + { Py_tp_dealloc, igraphmodule_EdgeSeq_dealloc }, + { Py_tp_members, igraphmodule_EdgeSeq_members }, + { Py_tp_methods, igraphmodule_EdgeSeq_methods }, + { Py_tp_getset, igraphmodule_EdgeSeq_getseters }, + { Py_tp_doc, (void*) igraphmodule_EdgeSeq_doc }, + + { Py_sq_length, igraphmodule_EdgeSeq_sq_length }, + { Py_sq_item, igraphmodule_EdgeSeq_sq_item }, + + { Py_mp_subscript, igraphmodule_EdgeSeq_get_attribute_values_mapping }, + { Py_mp_ass_subscript, igraphmodule_EdgeSeq_set_attribute_values_mapping }, + + { 0 } + }; + + PyType_Spec spec = { + "igraph._igraph.EdgeSeq", /* name */ + sizeof(igraphmodule_EdgeSeqObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ + slots, /* slots */ + }; + + igraphmodule_EdgeSeqType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_EdgeSeqType == 0; +} diff --git a/src/_igraph/edgeseqobject.h b/src/_igraph/edgeseqobject.h index 24307eef5..982368d69 100644 --- a/src/_igraph/edgeseqobject.h +++ b/src/_igraph/edgeseqobject.h @@ -39,24 +39,9 @@ typedef struct PyObject* weakreflist; } igraphmodule_EdgeSeqObject; -PyObject* igraphmodule_EdgeSeq_new(PyTypeObject *subtype, - PyObject *args, PyObject *kwds); -igraphmodule_EdgeSeqObject* igraphmodule_EdgeSeq_copy( - igraphmodule_EdgeSeqObject *o); -int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, - PyObject *args, PyObject *kwds); -void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self); +extern PyTypeObject* igraphmodule_EdgeSeqType; -Py_ssize_t igraphmodule_EdgeSeq_sq_length(igraphmodule_EdgeSeqObject *self); - -PyObject* igraphmodule_EdgeSeq_find(igraphmodule_EdgeSeqObject *self, - PyObject *args); -PyObject* igraphmodule_EdgeSeq_select(igraphmodule_EdgeSeqObject *self, - PyObject *args); - -PyObject* igraphmodule_EdgeSeq_get_graph(igraphmodule_EdgeSeqObject *self, - void* closure); - -extern PyTypeObject igraphmodule_EdgeSeqType; +int igraphmodule_EdgeSeq_Check(PyObject* obj); +int igraphmodule_EdgeSeq_register_type(void); #endif diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 1989fabea..6708e4705 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -57,56 +57,6 @@ PyTypeObject igraphmodule_GraphType; /** \defgroup python_interface_graph Graph object * \ingroup python_interface */ -/** - * \ingroup python_interface_internal - * \brief Initializes the internal structures in an \c igraph.Graph object's - * C representation. - * - * This function must be called whenever we create a new Graph object with - * \c tp_alloc - */ -void igraphmodule_Graph_init_internal(igraphmodule_GraphObject * self) -{ - if (!self) return; - - self->destructor = NULL; - self->weakreflist = NULL; -} - -/** - * \ingroup python_interface_graph - * \brief Creates a new igraph object in Python - * - * This function is called whenever a new \c igraph.Graph object is created in - * Python. An optional \c n parameter can be passed from Python, - * representing the number of vertices in the graph. If it is omitted, - * the default value is 0. - * - * Example call from Python: -\verbatim -g = igraph.Graph(5); -\endverbatim - * - * In fact, the parameters are processed by \c igraphmodule_Graph_init - * - * \return the new \c igraph.Graph object or NULL if an error occurred. - * - * \sa igraphmodule_Graph_init - * \sa igraph_empty - */ -PyObject *igraphmodule_Graph_new(PyTypeObject * type, PyObject * args, - PyObject * kwds) -{ - igraphmodule_GraphObject *self; - - self = (igraphmodule_GraphObject *) type->tp_alloc(type, 0); - RC_ALLOC("Graph", self); - - igraphmodule_Graph_init_internal(self); - - return (PyObject *) self; -} - /** * \ingroup python_interface_graph * \brief Clears the graph object's subobject (before deallocation) @@ -199,6 +149,9 @@ int igraphmodule_Graph_init(igraphmodule_GraphObject * self, igraph_vector_int_t edges_vector; igraph_bool_t edges_vector_owned = 0; + self->destructor = NULL; + self->weakreflist = NULL; + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|nOOO!", kwlist, &n, &edges, &dir, &PyCapsule_Type, &ptr_o)) @@ -741,8 +694,9 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); } - if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) + if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; + } if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, &return_single, 0)) { return NULL; @@ -16481,7 +16435,7 @@ PyTypeObject igraphmodule_GraphType = { 0, /* tp_dictoffset */ (initproc) igraphmodule_Graph_init, /* tp_init */ 0, /* tp_alloc */ - igraphmodule_Graph_new, /* tp_new */ + PyType_GenericNew, /* tp_new */ 0, /* tp_free */ }; diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 949b990bb..67521ad2c 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -50,7 +50,6 @@ typedef struct PyObject* weakreflist; } igraphmodule_GraphObject; -void igraphmodule_Graph_init_internal(igraphmodule_GraphObject *self); PyObject* igraphmodule_Graph_new(PyTypeObject *type, PyObject *args, PyObject *kwds); int igraphmodule_Graph_clear(igraphmodule_GraphObject *self); int igraphmodule_Graph_traverse(igraphmodule_GraphObject *self, visitproc visit, void *arg); diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 947541c54..8b9274113 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -836,19 +836,15 @@ PyObject* PyInit__igraph(void) INITERROR; } - /* Initialize VertexSeq, EdgeSeq */ - if (PyType_Ready(&igraphmodule_VertexSeqType) < 0) - INITERROR; - if (PyType_Ready(&igraphmodule_EdgeSeqType) < 0) - INITERROR; - /* Initialize types */ if ( igraphmodule_ARPACKOptions_register_type() || igraphmodule_BFSIter_register_type() || igraphmodule_DFSIter_register_type() || igraphmodule_Edge_register_type() || - igraphmodule_Vertex_register_type() + igraphmodule_EdgeSeq_register_type() || + igraphmodule_Vertex_register_type() || + igraphmodule_VertexSeq_register_type() ) { INITERROR; } @@ -869,9 +865,9 @@ PyObject* PyInit__igraph(void) PyModule_AddObject(m, "DFSIter", (PyObject*)igraphmodule_DFSIterType); PyModule_AddObject(m, "ARPACKOptions", (PyObject*)igraphmodule_ARPACKOptionsType); PyModule_AddObject(m, "Edge", (PyObject*)igraphmodule_EdgeType); - PyModule_AddObject(m, "EdgeSeq", (PyObject*)&igraphmodule_EdgeSeqType); + PyModule_AddObject(m, "EdgeSeq", (PyObject*)igraphmodule_EdgeSeqType); PyModule_AddObject(m, "Vertex", (PyObject*)igraphmodule_VertexType); - PyModule_AddObject(m, "VertexSeq", (PyObject*)&igraphmodule_VertexSeqType); + PyModule_AddObject(m, "VertexSeq", (PyObject*)igraphmodule_VertexSeqType); /* Internal error exception type */ igraphmodule_InternalError = @@ -879,7 +875,10 @@ PyObject* PyInit__igraph(void) PyModule_AddObject(m, "InternalError", igraphmodule_InternalError); /* ARPACK default options variable */ - igraphmodule_arpack_options_default = igraphmodule_ARPACKOptions_new(); + igraphmodule_arpack_options_default = PyObject_CallFunction((PyObject*) igraphmodule_ARPACKOptionsType, 0); + if (igraphmodule_arpack_options_default == NULL) + INITERROR; + PyModule_AddObject(m, "arpack_options", igraphmodule_arpack_options_default); /* Useful constants */ diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h index 724c1166a..56fd4d399 100644 --- a/src/_igraph/pyhelpers.h +++ b/src/_igraph/pyhelpers.h @@ -47,6 +47,21 @@ char* PyUnicode_CopyAsString(PyObject* string); #define PY_IGRAPH_WARN(msg) \ PyErr_WarnEx(PyExc_RuntimeWarning, (msg), 1) +/* Calling Py_DECREF() on heap-allocated types in tp_dealloc was not needed + * before Python 3.8 (see Python issue 35810) */ +#if PY_VERSION_HEX >= 0x03080000 + #define PY_FREE_AND_DECREF_TYPE(obj) { \ + PyTypeObject* _tp = Py_TYPE(obj); \ + ((freefunc)PyType_GetSlot(_tp, Py_tp_free))(obj); \ + Py_DECREF(_tp); \ + } +#else + #define PY_FREE_AND_DECREF_TYPE(obj) { \ + PyTypeObject* _tp = Py_TYPE(obj); \ + ((freefunc)PyType_GetSlot(_tp, Py_tp_free))(obj); \ + } +#endif + #define CHECK_SSIZE_T_RANGE(value, message) { \ if ((value) < 0) { \ PyErr_SetString(PyExc_ValueError, message " must be non-negative"); \ diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index eec5bdc6f..a51578fdc 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -22,7 +22,7 @@ */ -#define Py_LIMITED_API 0x03060000 +#define Py_LIMITED_API 0x03060100 #include "attributes.h" #include "convert.h" @@ -102,15 +102,17 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { */ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { igraphmodule_VertexObject* self; - self = PyObject_New(igraphmodule_VertexObject, igraphmodule_VertexType); + + self = (igraphmodule_VertexObject*) PyType_GenericNew(igraphmodule_VertexType, 0, 0); + if (self) { RC_ALLOC("Vertex", self); - Py_INCREF(igraphmodule_VertexType); Py_INCREF(gref); self->gref = gref; self->idx = idx; self->hash = -1; } + return (PyObject*)self; } @@ -128,14 +130,9 @@ static int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { * \brief Deallocates a Python representation of a given vertex object */ static void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { - PyTypeObject* tp = Py_TYPE(self); - - igraphmodule_Vertex_clear(self); - RC_DEALLOC("Vertex", self); - - PyObject_Del((PyObject*)self); - Py_DECREF(tp); /* needed because heap-allocated types are refcounted */ + igraphmodule_Vertex_clear(self); + PY_FREE_AND_DECREF_TYPE(self); } /** \ingroup python_interface_vertex diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index ff0e6c200..02160beb5 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -22,6 +22,8 @@ */ +#define Py_LIMITED_API 0x03060100 + #include "attributes.h" #include "common.h" #include "convert.h" @@ -37,27 +39,16 @@ * \defgroup python_interface_vertexseq Vertex sequence object */ -PyTypeObject igraphmodule_VertexSeqType; +PyTypeObject* igraphmodule_VertexSeqType; + +PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, PyObject *args); /** * \ingroup python_interface_vertexseq - * \brief Allocate a new vertex sequence object for a given graph - * \return the allocated PyObject + * \brief Checks whether the given Python object is a vertex sequence */ -PyObject* igraphmodule_VertexSeq_new(PyTypeObject *subtype, - PyObject *args, PyObject *kwds) { - igraphmodule_VertexSeqObject *o; - - o=(igraphmodule_VertexSeqObject*)PyType_GenericNew(subtype, args, kwds); - if (o == NULL) return NULL; - - igraph_vs_all(&o->vs); - o->gref=0; - o->weakreflist=0; - - RC_ALLOC("VertexSeq", o); - - return (PyObject*)o; +int igraphmodule_VertexSeq_Check(PyObject* obj) { + return obj ? PyObject_IsInstance(obj, (PyObject*)igraphmodule_VertexSeqType) : 0; } /** @@ -69,8 +60,10 @@ igraphmodule_VertexSeqObject* igraphmodule_VertexSeq_copy(igraphmodule_VertexSeqObject* o) { igraphmodule_VertexSeqObject *copy; - copy=(igraphmodule_VertexSeqObject*)PyType_GenericNew(Py_TYPE(o), 0, 0); - if (copy == NULL) return NULL; + copy = (igraphmodule_VertexSeqObject*) PyType_GenericNew(Py_TYPE(o), 0, 0); + if (copy == NULL) { + return NULL; + } if (igraph_vs_type(&o->vs) == IGRAPH_VS_VECTOR) { igraph_vector_int_t v; @@ -157,15 +150,19 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, * \brief Deallocates a Python representation of a given vertex sequence object */ void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self) { - if (self->weakreflist != NULL) + RC_DEALLOC("VertexSeq", self); + + if (self->weakreflist != NULL) { PyObject_ClearWeakRefs((PyObject *) self); + } + if (self->gref) { igraph_vs_destroy(&self->vs); Py_DECREF(self->gref); self->gref=0; } - Py_TYPE(self)->tp_free((PyObject*)self); - RC_DEALLOC("VertexSeq", self); + + PY_FREE_AND_DECREF_TYPE(self); } /** @@ -287,12 +284,24 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje case IGRAPH_VS_ALL: n = PyList_Size(values); result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } for (i = 0; i < n; i++) { - item = PyList_GET_ITEM(values, i); + item = PyList_GetItem(values, i); + if (!item) { + Py_DECREF(result); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; @@ -300,12 +309,24 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje case IGRAPH_VS_VECTORPTR: n = igraph_vector_int_size(self->vs.data.vecptr); result = PyList_New(n); - if (!result) return 0; + if (!result) { + return 0; + } for (i = 0; i < n; i++) { - item = PyList_GET_ITEM(values, VECTOR(*self->vs.data.vecptr)[i]); + item = PyList_GetItem(values, VECTOR(*self->vs.data.vecptr)[i]); + if (!item) { + Py_DECREF(result); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; @@ -316,9 +337,19 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje if (!result) return 0; for (i = 0; i < n; i++) { - item = PyList_GET_ITEM(values, self->vs.data.seq.from + i); + item = PyList_GetItem(values, self->vs.data.seq.from + i); + if (!item) { + Py_DECREF(result); + return 0; + } + Py_INCREF(item); - PyList_SET_ITEM(result, i, item); + + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } break; @@ -332,29 +363,44 @@ PyObject* igraphmodule_VertexSeq_get_attribute_values(igraphmodule_VertexSeqObje PyObject* igraphmodule_VertexSeq_get_attribute_values_mapping(igraphmodule_VertexSeqObject *self, PyObject *o) { Py_ssize_t index; - - /* Handle integer indices according to the sequence protocol */ - if (PyIndex_Check(o)) { - index = PyNumber_AsSsize_t(o, 0); - return igraphmodule_VertexSeq_sq_item(self, index); - } + PyObject* index_o; /* Handle strings according to the mapping protocol */ - if (PyBaseString_Check(o)) + if (PyBaseString_Check(o)) { return igraphmodule_VertexSeq_get_attribute_values(self, o); + } /* Handle iterables and slices by calling the select() method */ if (PySlice_Check(o) || PyObject_HasAttrString(o, "__iter__")) { PyObject *result, *args; - args = Py_BuildValue("(O)", o); + args = PyTuple_Pack(1, o); - if (!args) + if (!args) { return NULL; + } + result = igraphmodule_VertexSeq_select(self, args); Py_DECREF(args); + return result; } + /* Handle integer indices according to the sequence protocol */ + index_o = PyNumber_Index(o); + if (index_o) { + index = PyLong_AsSsize_t(index_o); + if (PyErr_Occurred()) { + Py_DECREF(index_o); + return NULL; + } else { + Py_DECREF(index_o); + return igraphmodule_VertexSeq_sq_item(self, index); + } + } else { + /* clear TypeError raised by PyNumber_Index() */ + PyErr_Clear(); + } + /* Handle everything else according to the mapping protocol */ return igraphmodule_VertexSeq_get_attribute_values(self, o); } @@ -389,16 +435,25 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb * single element (the value itself) and then call ourselves again */ int result; PyObject *newList = PyList_New(1); - if (newList == 0) return -1; + if (newList == 0) { + return -1; + } + Py_INCREF(values); - PyList_SET_ITEM(newList, 0, values); /* reference stolen here */ + if (PyList_SetItem(newList, 0, values)) { /* reference stolen here */ + return -1; + } + result = igraphmodule_VertexSeq_set_attribute_values_mapping(self, attrname, newList); Py_DECREF(newList); + return result; } - n=PySequence_Size(values); - if (n < 0) return -1; + n = PySequence_Size(values); + if (n < 0) { + return -1; + } if (igraph_vs_type(&self->vs) == IGRAPH_VS_ALL) { no_of_nodes = igraph_vcount(&gr->g); @@ -429,9 +484,16 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb for (i = 0, j = 0; i < no_of_nodes; i++, j++) { if (j == n) j = 0; item = PySequence_GetItem(values, j); - if (item == 0) { Py_DECREF(list); return -1; } + if (item == 0) { + Py_DECREF(list); + return -1; + } /* No need to Py_INCREF(item), PySequence_GetItem returns a new reference */ - PyList_SET_ITEM(list, i, item); + if (PyList_SetItem(list, i, item)) { + Py_DECREF(item); + Py_DECREF(list); + return -1; + } /* PyList_SET_ITEM stole a reference to the item automatically */ } if (PyDict_SetItem(dict, attrname, list)) { @@ -487,19 +549,31 @@ int igraphmodule_VertexSeq_set_attribute_values_mapping(igraphmodule_VertexSeqOb igraph_vector_int_destroy(&vs); return -1; } - for (i=0; igref; - result=igraphmodule_VertexSeq_copy(self); - if (result==0) + gr = self->gref; + result = igraphmodule_VertexSeq_copy(self); + if (result == 0) { return NULL; + } /* First, filter by positional arguments */ n = PyTuple_Size(args); - for (i=0; ivs); igraph_vs_none(&result->vs); @@ -700,8 +777,14 @@ PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, } m = igraph_vector_int_size(&v2); for (; i= m || idx < 0) { + Py_DECREF(result); PyErr_SetString(PyExc_ValueError, "vertex index out of range"); igraph_vector_int_destroy(&v); igraph_vector_int_destroy(&v2); @@ -880,6 +964,7 @@ PyObject* igraphmodule_VertexSeq_get_indices(igraphmodule_VertexSeqObject* self, igraphmodule_handle_igraph_error(); return 0; } + if (igraph_vs_as_vector(&gr->g, self->vs, &vs)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&vs); @@ -900,8 +985,11 @@ PyObject* igraphmodule_VertexSeq__name_index(igraphmodule_VertexSeqObject* self, void* closure) { igraphmodule_GraphObject *gr = self->gref; PyObject* result = ATTR_NAME_INDEX(&gr->g); - if (result == 0) + + if (result == 0) { Py_RETURN_NONE; + } + Py_INCREF(result); return result; } @@ -961,39 +1049,6 @@ PyMethodDef igraphmodule_VertexSeq_methods[] = { {NULL} }; -/** - * \ingroup python_interface_vertexseq - * This is the collection of functions necessary to implement the - * vertex sequence as a real sequence (e.g. allowing to reference - * vertices by indices) - */ -static PySequenceMethods igraphmodule_VertexSeq_as_sequence = { - (lenfunc)igraphmodule_VertexSeq_sq_length, - 0, /* sq_concat */ - 0, /* sq_repeat */ - (ssizeargfunc)igraphmodule_VertexSeq_sq_item, /* sq_item */ - 0, /* sq_slice */ - 0, /* sq_ass_item */ - 0, /* sq_ass_slice */ - 0, /* sq_contains */ - 0, /* sq_inplace_concat */ - 0, /* sq_inplace_repeat */ -}; - -/** - * \ingroup python_interface_vertexseq - * This is the collection of functions necessary to implement the - * vertex sequence as a mapping (which maps attribute names to values) - */ -static PyMappingMethods igraphmodule_VertexSeq_as_mapping = { - /* this must be null, otherwise it f.cks up sq_length when inherited */ - (lenfunc) 0, - /* returns the values of an attribute by name */ - (binaryfunc) igraphmodule_VertexSeq_get_attribute_values_mapping, - /* sets the values of an attribute by name */ - (objobjargproc) igraphmodule_VertexSeq_set_attribute_values_mapping, -}; - /** * \ingroup python_interface_vertexseq * Getter/setter table for the \c igraph.VertexSeq object @@ -1011,57 +1066,48 @@ PyGetSetDef igraphmodule_VertexSeq_getseters[] = { {NULL} }; -/** \ingroup python_interface_vertexseq - * Python type object referencing the methods Python calls when it performs various operations on - * a vertex sequence of a graph +/** + * \ingroup python_interface_vertexseq + * Member table for the \c igraph.VertexSeq object */ -PyTypeObject igraphmodule_VertexSeqType = -{ - PyVarObject_HEAD_INIT(0, 0) - "igraph._igraph.VertexSeq", /* tp_name */ - sizeof(igraphmodule_VertexSeqObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor)igraphmodule_VertexSeq_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - 0, /* tp_repr */ - 0, /* tp_as_number */ - &igraphmodule_VertexSeq_as_sequence, /* tp_as_sequence */ - &igraphmodule_VertexSeq_as_mapping, /* tp_as_mapping */ - 0, /* tp_hash */ - 0, /* tp_call */ - 0, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ +PyMemberDef igraphmodule_VertexSeq_members[] = { + {"__weaklistoffset__", T_PYSSIZET, offsetof(igraphmodule_VertexSeqObject, weakreflist), READONLY}, + { 0 } +}; + +PyDoc_STRVAR( + igraphmodule_VertexSeq_doc, "Low-level representation of a vertex sequence.\n\n" /* tp_doc */ "Don't use it directly, use L{igraph.VertexSeq} instead.\n\n" - "@deffield ref: Reference", - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - offsetof(igraphmodule_VertexSeqObject, weakreflist), /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - igraphmodule_VertexSeq_methods, /* tp_methods */ - 0, /* tp_members */ - igraphmodule_VertexSeq_getseters, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc) igraphmodule_VertexSeq_init, /* tp_init */ - 0, /* tp_alloc */ - (newfunc) igraphmodule_VertexSeq_new, /* tp_new */ - 0, /* tp_free */ - 0, /* tp_is_gc */ - 0, /* tp_bases */ - 0, /* tp_mro */ - 0, /* tp_cache */ - 0, /* tp_subclasses */ - 0, /* tp_weakreflist */ -}; + "@deffield ref: Reference" +); + +int igraphmodule_VertexSeq_register_type() { + PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_VertexSeq_init }, + { Py_tp_dealloc, igraphmodule_VertexSeq_dealloc }, + { Py_tp_members, igraphmodule_VertexSeq_members }, + { Py_tp_methods, igraphmodule_VertexSeq_methods }, + { Py_tp_getset, igraphmodule_VertexSeq_getseters }, + { Py_tp_doc, (void*) igraphmodule_VertexSeq_doc }, + + { Py_sq_length, igraphmodule_VertexSeq_sq_length }, + { Py_sq_item, igraphmodule_VertexSeq_sq_item }, + + { Py_mp_subscript, igraphmodule_VertexSeq_get_attribute_values_mapping }, + { Py_mp_ass_subscript, igraphmodule_VertexSeq_set_attribute_values_mapping }, + + { 0 } + }; + + PyType_Spec spec = { + "igraph._igraph.VertexSeq", /* name */ + sizeof(igraphmodule_VertexSeqObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* flags */ + slots, /* slots */ + }; + + igraphmodule_VertexSeqType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_VertexSeqType == 0; +} diff --git a/src/_igraph/vertexseqobject.h b/src/_igraph/vertexseqobject.h index de195bb78..e27c47f08 100644 --- a/src/_igraph/vertexseqobject.h +++ b/src/_igraph/vertexseqobject.h @@ -38,24 +38,9 @@ typedef struct { PyObject* weakreflist; } igraphmodule_VertexSeqObject; -PyObject* igraphmodule_VertexSeq_new(PyTypeObject *subtype, - PyObject* args, PyObject* kwds); -int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject* self, - PyObject* args, PyObject* kwds); -void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self); +extern PyTypeObject* igraphmodule_VertexSeqType; -Py_ssize_t igraphmodule_VertexSeq_sq_length(igraphmodule_VertexSeqObject *self); - -PyObject* igraphmodule_VertexSeq_find(igraphmodule_VertexSeqObject *self, - PyObject *args); -PyObject* igraphmodule_VertexSeq_select(igraphmodule_VertexSeqObject *self, - PyObject *args); - -int igraphmodule_VertexSeq_to_vector_t(igraphmodule_VertexSeqObject *self, - igraph_vector_int_t *v); -PyObject* igraphmodule_VertexSeq_get_graph(igraphmodule_VertexSeqObject *self, - void* closure); - -extern PyTypeObject igraphmodule_VertexSeqType; +int igraphmodule_VertexSeq_Check(PyObject* obj); +int igraphmodule_VertexSeq_register_type(void); #endif diff --git a/test.sh b/test.sh index 64d9b8789..b3e124265 100755 --- a/test.sh +++ b/test.sh @@ -7,9 +7,11 @@ PYTHON=python3 set -e CLEAN=0 +PYTEST_ARGS= VENV_DIR=.venv -while getopts ":ce:" OPTION; do +while getopts ":ce:k:" OPTION; do + echo "$OPTION" case $OPTION in c) CLEAN=1 @@ -17,12 +19,15 @@ while getopts ":ce:" OPTION; do e) VENV_DIR=$OPTARG ;; + k) + PYTEST_ARGS="${PYTEST_ARGS} -k $OPTARG" + ;; \?) echo "Usage: $0 [-c]" ;; esac - shift $((OPTIND -1)) done +shift $((OPTIND -1)) if [ ! -d $VENV_DIR ]; then $PYTHON -m venv $VENV_DIR @@ -36,5 +41,5 @@ fi $VENV_DIR/bin/python setup.py build $VENV_DIR/bin/pip install --use-feature=in-tree-build .[plotting,test] -$VENV_DIR/bin/pytest tests +$VENV_DIR/bin/pytest tests ${PYTEST_ARGS} From b83dc3dc17782641db42d535e897135a1489f577 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 12 Sep 2021 00:56:45 +0200 Subject: [PATCH 0409/1681] fix: remove some deprecated constructs; uniformize deprecation and runtime warnings --- src/_igraph/convert.c | 7 ++- src/_igraph/error.c | 4 +- src/_igraph/graphobject.c | 108 +++++++++++++++---------------------- src/_igraph/igraphmodule.c | 2 +- src/_igraph/indexing.c | 7 +-- 5 files changed, 55 insertions(+), 73 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 762d60366..8fabe973a 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -131,8 +131,7 @@ int igraphmodule_PyObject_to_enum(PyObject *o, free(s); if (best_unique) { - PyErr_Warn( - PyExc_DeprecationWarning, + PY_IGRAPH_DEPRECATED( "Partial string matches of enum members are deprecated since igraph 0.9.3; " "use strings that identify an enum member unambiguously." ); @@ -2339,7 +2338,7 @@ int igraphmodule_PyList_to_matrix_t_with_minimum_column_count(PyObject *o, igrap } else if (PyFloat_Check(item)) { MATRIX(*m, i, j) = PyFloat_AsDouble(item); } else if (!was_warned) { - PyErr_Warn(PyExc_Warning, "non-numeric value in matrix ignored"); + PY_IGRAPH_WARN("non-numeric value in matrix ignored"); was_warned=1; } Py_DECREF(item); @@ -2417,7 +2416,7 @@ int igraphmodule_PyList_to_matrix_int_t_with_minimum_column_count(PyObject *o, i } if (!ok && !was_warned) { - PyErr_Warn(PyExc_Warning, "non-numeric value in matrix ignored"); + PY_IGRAPH_WARN("non-numeric value in matrix ignored"); was_warned = 1; } diff --git a/src/_igraph/error.c b/src/_igraph/error.c index 2954ad7b7..8666ce02d 100644 --- a/src/_igraph/error.c +++ b/src/_igraph/error.c @@ -21,6 +21,8 @@ */ #include "error.h" +#include "pyhelpers.h" + #include /** \ingroup python_interface_errors @@ -60,7 +62,7 @@ void igraphmodule_igraph_warning_hook(const char *reason, const char *file, int line, int igraph_errno) { char buf[4096]; snprintf(buf, sizeof(buf), "%s at %s:%i", reason, file, line); - PyErr_Warn(PyExc_RuntimeWarning, buf); + PY_IGRAPH_WARN(buf); } /** diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 6708e4705..c1c361181 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -639,11 +639,9 @@ PyObject *igraphmodule_Graph_delete_edges(igraphmodule_GraphObject * self, /* no arguments means delete all. */ - /*Py_None also means all for now, but it is deprecated */ + /* Py_None means "do nothing" since igraph 0.10 */ if (list == Py_None) { - PyErr_Warn(PyExc_DeprecationWarning, "Graph.delete_vertices(None) is " - "deprecated since igraph 0.8.3, please use " - "Graph.delete_vertices() instead"); + Py_RETURN_NONE; } /* this already converts no arguments and Py_None to all vertices */ @@ -676,24 +674,18 @@ PyObject *igraphmodule_Graph_degree(igraphmodule_GraphObject * self, { PyObject *list = Py_None; PyObject *loops = Py_True; - PyObject *dtype_o = Py_None; PyObject *dmode_o = Py_None; igraph_neimode_t dmode = IGRAPH_ALL; igraph_vector_int_t result; igraph_vs_t vs; igraph_bool_t return_single = 0; - static char *kwlist[] = { "vertices", "mode", "loops", "type", NULL }; + static char *kwlist[] = { "vertices", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, - &list, &dmode_o, &loops, &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, + &list, &dmode_o, &loops)) return NULL; - if (dmode_o == Py_None && dtype_o != Py_None) { - dmode_o = dtype_o; - PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); - } - if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; } @@ -813,7 +805,6 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, { PyObject *list = Py_None; PyObject *loops = Py_True; - PyObject *dtype_o = Py_None; PyObject *dmode_o = Py_None; PyObject *weights_o = Py_None; igraph_neimode_t dmode = IGRAPH_ALL; @@ -821,21 +812,15 @@ PyObject *igraphmodule_Graph_strength(igraphmodule_GraphObject * self, igraph_vs_t vs; igraph_bool_t return_single = 0; - static char *kwlist[] = { "vertices", "mode", "loops", "weights", - "type", NULL }; + static char *kwlist[] = { "vertices", "mode", "loops", "weights", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOO", kwlist, - &list, &dmode_o, &loops, &weights_o, - &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, + &list, &dmode_o, &loops, &weights_o)) return NULL; - if (dmode_o == Py_None && dtype_o != Py_None) { - dmode_o = dtype_o; - PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); - } - - if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) + if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { return NULL; + } if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, &return_single, 0)) { igraphmodule_handle_igraph_error(); @@ -909,33 +894,27 @@ PyObject *igraphmodule_Graph_maxdegree(igraphmodule_GraphObject * self, { PyObject *list = Py_None; igraph_neimode_t dmode = IGRAPH_ALL; - PyObject *dtype_o = Py_None; PyObject *dmode_o = Py_None; PyObject *loops = Py_False; igraph_integer_t result; igraph_vs_t vs; igraph_bool_t return_single = 0; - static char *kwlist[] = { "vertices", "mode", "loops", "type", NULL }; + static char *kwlist[] = { "vertices", "mode", "loops", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOO", kwlist, - &list, &dmode_o, &loops, &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOO", kwlist, &list, &dmode_o, &loops)) return NULL; - if (dmode_o == Py_None && dtype_o != Py_None) { - dmode_o = dtype_o; - PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); + if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { + return NULL; } - if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) return NULL; - if (igraphmodule_PyObject_to_vs_t(list, &vs, &self->g, &return_single, 0)) { igraphmodule_handle_igraph_error(); return NULL; } - if (igraph_maxdegree(&self->g, &result, vs, - dmode, PyObject_IsTrue(loops))) { + if (igraph_maxdegree(&self->g, &result, vs, dmode, PyObject_IsTrue(loops))) { igraphmodule_handle_igraph_error(); igraph_vs_destroy(&vs); return NULL; @@ -1168,30 +1147,28 @@ PyObject *igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list, *dtype_o=Py_None, *dmode_o=Py_None, *index_o; + PyObject *list, *dmode_o = Py_None, *index_o; igraph_neimode_t dmode = IGRAPH_ALL; igraph_integer_t idx; igraph_vector_int_t result; - static char *kwlist[] = { "vertex", "mode", "type", NULL }; + static char *kwlist[] = { "vertex", "mode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, - &index_o, &dmode_o, &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &index_o, &dmode_o)) return NULL; - if (dmode_o == Py_None && dtype_o != Py_None) { - dmode_o = dtype_o; - PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); + if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { + return NULL; } - if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; + } - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) + if (igraph_vector_int_init(&result, 1)) { + igraphmodule_handle_igraph_error(); return NULL; - - if (igraph_vector_int_init(&result, 1)) - return igraphmodule_handle_igraph_error(); + } if (igraph_neighbors(&self->g, &result, idx, dmode)) { igraphmodule_handle_igraph_error(); @@ -1219,29 +1196,29 @@ PyObject *igraphmodule_Graph_neighbors(igraphmodule_GraphObject * self, PyObject *igraphmodule_Graph_incident(igraphmodule_GraphObject * self, PyObject * args, PyObject * kwds) { - PyObject *list, *dmode_o = Py_None, *dtype_o = Py_None, *index_o; + PyObject *list, *dmode_o = Py_None, *index_o; igraph_neimode_t dmode = IGRAPH_OUT; igraph_integer_t idx; igraph_vector_int_t result; - static char *kwlist[] = { "vertex", "mode", "type", NULL }; + static char *kwlist[] = { "vertex", "mode", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OO", kwlist, - &index_o, &dmode_o, &dtype_o)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|O", kwlist, &index_o, &dmode_o)) return NULL; - if (dmode_o == Py_None && dtype_o != Py_None) { - dmode_o = dtype_o; - PY_IGRAPH_DEPRECATED("type=... keyword argument is deprecated since igraph 0.6, use mode=... instead"); + if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) { + return NULL; } - if (igraphmodule_PyObject_to_neimode_t(dmode_o, &dmode)) + if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) { return NULL; + } - if (igraphmodule_PyObject_to_vid(index_o, &idx, &self->g)) + if (igraph_vector_int_init(&result, 1)) { + igraphmodule_handle_igraph_error(); return NULL; + } - igraph_vector_int_init(&result, 1); if (igraph_incident(&self->g, &result, idx, dmode)) { igraphmodule_handle_igraph_error(); igraph_vector_int_destroy(&result); @@ -7028,10 +7005,11 @@ PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, if (fixed_o != 0 && fixed_o != Py_None) { /* Apparently the "fixed" argument does not do anything in the DrL * implementation so we throw a warning if the user tries to use it */ - PyErr_Warn(PyExc_DeprecationWarning, "The fixed=... argument of the DrL " - "layout is ignored; it is kept only for sake of backwards " - "compatibility. The DrL layout algorithm does not support " - "permanently fixed nodes."); + PY_IGRAPH_DEPRECATED( + "The fixed=... argument of the DrL layout is ignored; it is kept only " + "for sake of backwards compatibility. The DrL layout algorithm does not " + "support permanently fixed nodes." + ); fixed = (igraph_vector_bool_t*)malloc(sizeof(igraph_vector_bool_t)); if (!fixed) { PyErr_NoMemory(); @@ -7890,8 +7868,10 @@ PyObject *igraphmodule_Graph_to_directed(igraphmodule_GraphObject * self, mode = IGRAPH_TO_DIRECTED_MUTUAL; } else { mode = PyObject_IsTrue(mutual_o) ? IGRAPH_TO_DIRECTED_MUTUAL : IGRAPH_TO_DIRECTED_ARBITRARY; - PyErr_Warn(PyExc_DeprecationWarning, "The 'mutual' argument is deprecated since " - "igraph 0.9.3, please use mode=... instead"); + PY_IGRAPH_DEPRECATED( + "The 'mutual' argument is deprecated since igraph 0.9.3, please use " + "mode=... instead" + ); } } else { if (igraphmodule_PyObject_to_to_directed_t(mode_o, &mode)) { diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 8b9274113..d4dfa1e7d 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -264,7 +264,7 @@ PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwd } if (PySequence_Size(o) > 2) { - PyErr_Warn(PyExc_Warning, "vertex with more than 2 coordinates found, considering only the first 2"); + PY_IGRAPH_WARN("vertex with more than 2 coordinates found, considering only the first 2"); } } else { PyErr_SetString(PyExc_TypeError, "vertex with less than 2 coordinates found"); diff --git a/src/_igraph/indexing.c b/src/_igraph/indexing.c index 70b76ef94..7dedd2677 100644 --- a/src/_igraph/indexing.c +++ b/src/_igraph/indexing.c @@ -349,9 +349,10 @@ static int igraphmodule_i_Graph_adjmatrix_set_index_row(igraph_t* graph, IGRAPH_VIT_NEXT(vit); } if (!IGRAPH_VIT_END(vit)) { - PyErr_WarnEx(PyExc_RuntimeWarning, - "iterable was shorter than the number of vertices in the vertex " - "sequence", 1); + PY_IGRAPH_WARN( + "iterable was shorter than the number of vertices in the vertex " + "sequence" + ); } } else { /* The new value is not an iterable; setting the same value for From 70be1404339bac1bb4b7b636887962c81420d209 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 14:51:43 +0200 Subject: [PATCH 0410/1681] refactor: module now uses Python's limited API only and thus we can ship a single wheel per platform --- setup.py | 2 +- src/_igraph/arpackobject.h | 1 - src/_igraph/attributes.c | 2 - src/_igraph/bfsiter.c | 2 - src/_igraph/convert.c | 152 +++++++++++++------- src/_igraph/dfsiter.c | 2 - src/_igraph/edgeobject.c | 2 - src/_igraph/edgeseqobject.c | 4 +- src/_igraph/graphobject.c | 260 ++++++++++++++-------------------- src/_igraph/graphobject.h | 175 +---------------------- src/_igraph/igraphmodule.c | 5 +- src/_igraph/operators.c | 52 ++++--- src/_igraph/preamble.h | 4 + src/_igraph/pyhelpers.c | 24 +++- src/_igraph/vertexobject.c | 2 - src/_igraph/vertexseqobject.c | 4 +- src/igraph/utils.py | 4 - tests/test_basic.py | 64 ++++++++- 18 files changed, 335 insertions(+), 426 deletions(-) diff --git a/setup.py b/setup.py index c7dbb458c..5ba9f0e12 100644 --- a/setup.py +++ b/setup.py @@ -792,7 +792,7 @@ def use_educated_guess(self) -> None: # Define the extension sources = glob.glob(os.path.join("src", "_igraph", "*.c")) sources.append(os.path.join("src", "_igraph", "force_cpp_linker.cpp")) -igraph_extension = Extension("igraph._igraph", sources) +igraph_extension = Extension("igraph._igraph", sources, py_limited_api=True) description = """Python interface to the igraph high performance graph library, primarily aimed at complex network research and analysis. diff --git a/src/_igraph/arpackobject.h b/src/_igraph/arpackobject.h index affc94718..18ab29d4c 100644 --- a/src/_igraph/arpackobject.h +++ b/src/_igraph/arpackobject.h @@ -23,7 +23,6 @@ #ifndef PYTHON_ARPACKOBJECT_H #define PYTHON_ARPACKOBJECT_H -#define Py_LIMITED_API 0x03060100 #include "preamble.h" #include diff --git a/src/_igraph/attributes.c b/src/_igraph/attributes.c index 86400f521..dfada5c38 100644 --- a/src/_igraph/attributes.c +++ b/src/_igraph/attributes.c @@ -20,8 +20,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "attributes.h" #include "common.h" #include "convert.h" diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index bb26e23e1..a250bbedd 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -20,8 +20,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "bfsiter.h" #include "common.h" #include "convert.h" diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 8fabe973a..b790689bd 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -371,7 +371,15 @@ int igraphmodule_PyObject_to_eigen_which_t(PyObject *object, /* Exception set already by PyUnicode_AsEncodedString */ return -1; } - kv = strdup(PyBytes_AS_STRING(temp_bytes)); + kv = PyBytes_AsString(temp_bytes); + if (kv == 0) { + /* Exception set already by PyBytes_AsString */ + return -1; + } + kv = strdup(kv); + if (kv == 0) { + PyErr_SetString(PyExc_MemoryError, "Not enough memory"); + } Py_DECREF(temp_bytes); if (!strcasecmp(kv, "pos")) { igraphmodule_PyObject_to_enum(value, eigen_which_position_tt, @@ -779,9 +787,8 @@ int igraphmodule_PyObject_to_igraph_t(PyObject *o, igraph_t **result) { if (o == Py_None) return 0; - if (!PyObject_TypeCheck(o, &igraphmodule_GraphType)) { - PyErr_Format(PyExc_TypeError, - "expected graph object, got %s", o->ob_type->tp_name); + if (!PyObject_TypeCheck(o, igraphmodule_GraphType)) { + PyErr_Format(PyExc_TypeError, "expected graph object, got %R", Py_TYPE(o)); return 1; } @@ -872,31 +879,31 @@ int igraphmodule_PyObject_to_integer_t(PyObject *object, igraph_integer_t *v) { * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_real_t(PyObject *object, igraph_real_t *v) { -#ifdef PYPY_VERSION - /* PyFloatObject is not defined in pypy, but PyFloat_AS_DOUBLE() is - * supported on PyObject: /pypy/module/cpyext/floatobject.py. Also, - * don't worry, the typedef is local to this function. */ - typedef PyObject PyFloatObject; -#endif /* PYPY_VERSION */ + igraph_real_t value; if (object == NULL) { } else if (PyLong_Check(object)) { - *v = PyLong_AsDouble(object); - return 0; + value = PyLong_AsDouble(object); } else if (PyFloat_Check(object)) { - *v = PyFloat_AS_DOUBLE((PyFloatObject*)object); - return 0; + value = PyFloat_AsDouble(object); } else if (PyNumber_Check(object)) { PyObject *i = PyNumber_Float(object); if (i == NULL) { return 1; } - *v = PyFloat_AS_DOUBLE((PyFloatObject*)i); + value = PyFloat_AsDouble(i); Py_DECREF(i); + } else { + PyErr_BadArgument(); + return 1; + } + + if (PyErr_Occurred()) { + return 1; + } else { + *v = value; return 0; } - PyErr_BadArgument(); - return 1; } /** @@ -1346,7 +1353,7 @@ PyObject* igraphmodule_vector_bool_t_to_PyList(const igraph_vector_bool_t *v) { for (i = 0; i < n; i++) { item = VECTOR(*v)[i] ? Py_True : Py_False; Py_INCREF(item); - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } return list; @@ -1379,7 +1386,7 @@ PyObject* igraphmodule_vector_t_to_PyList(const igraph_vector_t *v, igraphmodule Py_DECREF(list); return NULL; } - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } return list; @@ -1412,7 +1419,7 @@ PyObject* igraphmodule_vector_int_t_to_PyList(const igraph_vector_int_t *v) { Py_DECREF(list); return NULL; } - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } return list; @@ -1446,7 +1453,7 @@ PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t for (i = 0; i < n; i++) { val = VECTOR(*v)[i]; if (val == nanvalue) { - item = Py_BuildValue("d", NAN); + item = PyFloat_FromDouble(NAN); } else { item = igraphmodule_integer_t_to_PyObject(VECTOR(*v)[i]); } @@ -1454,7 +1461,7 @@ PyObject* igraphmodule_vector_int_t_to_PyList_with_nan(const igraph_vector_int_t Py_DECREF(list); return NULL; } - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } return list; @@ -1490,7 +1497,7 @@ PyObject* igraphmodule_vector_t_to_PyTuple(const igraph_vector_t *v, igraphmodul return NULL; } - PyTuple_SET_ITEM(tuple, i, item); + PyTuple_SetItem(tuple, i, item); /* will not fail */ } return tuple; @@ -1523,7 +1530,7 @@ PyObject* igraphmodule_vector_int_t_to_PyTuple(const igraph_vector_int_t *v) { Py_DECREF(tuple); return NULL; } - PyTuple_SET_ITEM(tuple, i, item); + PyTuple_SetItem(tuple, i, item); /* will not fail */ } return tuple; @@ -1579,7 +1586,7 @@ PyObject* igraphmodule_vector_int_t_to_PyList_pairs(const igraph_vector_int_t *v Py_DECREF(second); first = second = 0; - PyList_SET_ITEM(list, i, pair); + PyList_SetItem(list, i, pair); /* will not fail */ } return list; @@ -1609,8 +1616,7 @@ int igraphmodule_PyObject_to_edgelist( PyObject *list, igraph_vector_int_t *v, igraph_t *graph, igraph_bool_t* list_is_owned ) { - PyObject *item, *i1, *i2, *it; - Py_buffer *buffer; + PyObject *item, *i1, *i2, *it, *expected; int ok; igraph_integer_t idx1=0, idx2=0; @@ -1626,35 +1632,75 @@ int igraphmodule_PyObject_to_edgelist( * way items are laid out in an igraph_vector_int_t, and that's an implementation * detail that we don't want to commit ourselves to */ if (PyMemoryView_Check(list)) { - buffer = PyMemoryView_GET_BUFFER(list); - - if (buffer->itemsize != sizeof(igraph_integer_t)) { + item = PyObject_GetAttrString(list, "itemsize"); + expected = PyLong_FromSize_t(sizeof(igraph_integer_t)); + ok = item && PyObject_RichCompareBool(item, expected, Py_EQ); + Py_XDECREF(expected); + Py_XDECREF(item); + if (!ok) { PyErr_SetString( PyExc_TypeError, "item size of buffer must match the size of igraph_integer_t" ); return 1; } - if (buffer->ndim != 2) { + item = PyObject_GetAttrString(list, "ndim"); + expected = PyLong_FromSize_t(2); + ok = item && PyObject_RichCompareBool(item, expected, Py_EQ); + Py_XDECREF(expected); + Py_XDECREF(item); + if (!ok) { PyErr_SetString(PyExc_TypeError, "edge list buffers must be two-dimensional"); return 1; } - if (buffer->shape[1] != 2) { + item = PyObject_GetAttrString(list, "shape"); + it = item && PySequence_Check(item) ? PySequence_GetItem(item, 1) : 0; + expected = PyLong_FromSize_t(2); + ok = it && PyObject_RichCompareBool(it, expected, Py_EQ); + Py_XDECREF(expected); + Py_XDECREF(item); + Py_XDECREF(it); + if (!ok) { PyErr_SetString(PyExc_TypeError, "edge list buffers must have two columns"); return 1; } - if (buffer->strides[0] != 2 * buffer->itemsize || - buffer->strides[1] != buffer->itemsize) { + item = PyObject_GetAttrString(list, "c_contiguous"); + ok = item == Py_True; + Py_XDECREF(item); + if (!ok) { PyErr_SetString(PyExc_TypeError, "edge list buffers must be contiguous"); return 1; } - igraph_vector_int_view(v, buffer->buf, buffer->len / buffer->itemsize); + /* If we are allowed to use the entire Python API, we can extract the buffer + * from the memoryview here and return a _view_ into the buffer so we can + * avoid copying. However, if we need to use the limited Python API, we + * cannot get access to the buffer so we need to convert the memoryview + * into a list first, and then cast that list into a _real_ igraph vector. + */ + { +#ifdef PY_IGRAPH_ALLOW_ENTIRE_PYTHON_API + Py_buffer *buffer = PyMemoryView_GET_BUFFER(item); + igraph_vector_int_view(v, buffer->buf, buffer->len / buffer->itemsize); + + if (list_is_owned) { + *list_is_owned = 0; + } +#else + PyObject *unfolded_list = PyObject_CallMethod(list, "tolist", 0); + if (!unfolded_list) { + return 1; + } + + if (igraphmodule_PyObject_to_edgelist(unfolded_list, v, graph, list_is_owned)) { + Py_DECREF(unfolded_list); + return 1; + } - if (list_is_owned) { - *list_is_owned = 0; + Py_DECREF(unfolded_list); +#endif } return 0; @@ -1673,14 +1719,10 @@ int igraphmodule_PyObject_to_edgelist( ok = 1; if (!PySequence_Check(item) || PySequence_Size(item) != 2) { PyErr_SetString(PyExc_TypeError, "iterable must return pairs of integers or strings"); - ok=0; + ok = 0; } else { - i1 = PySequence_ITEM(item, 0); - if (i1 == 0) { - i2 = 0; - } else { - i2 = PySequence_ITEM(item, 1); - } + i1 = PySequence_GetItem(item, 0); + i2 = i1 ? PySequence_GetItem(item, 1) : 0; ok = (i1 != 0 && i2 != 0); ok = ok && !igraphmodule_PyObject_to_vid(i1, &idx1, graph); ok = ok && !igraphmodule_PyObject_to_vid(i2, &idx2, graph); @@ -2100,7 +2142,7 @@ PyObject* igraphmodule_vector_int_t_pair_to_PyList(const igraph_vector_int_t *v1 Py_DECREF(second); first = second = 0; - PyList_SET_ITEM(list, i, pair); + PyList_SetItem(list, i, pair); /* will not fail */ } return list; @@ -2148,10 +2190,10 @@ PyObject* igraphmodule_matrix_t_to_PyList(const igraph_matrix_t *m, return NULL; } - PyList_SET_ITEM(row, j, item); + PyList_SetItem(row, j, item); /* will not fail */ } - PyList_SET_ITEM(list, i, row); + PyList_SetItem(list, i, row); /* will not fail */ } // return the list @@ -2198,10 +2240,10 @@ PyObject* igraphmodule_matrix_int_t_to_PyList(const igraph_matrix_int_t *m) { return NULL; } - PyList_SET_ITEM(row, j, item); + PyList_SetItem(row, j, item); /* will not fail */ } - PyList_SET_ITEM(list, i, row); + PyList_SetItem(list, i, row); /* will not fail */ } // return the list @@ -2236,7 +2278,7 @@ PyObject* igraphmodule_vector_ptr_t_to_PyList(const igraph_vector_ptr_t *v, Py_DECREF(list); return NULL; } - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } return list; @@ -2269,7 +2311,7 @@ PyObject* igraphmodule_vector_int_ptr_t_to_PyList(const igraph_vector_ptr_t *v) Py_DECREF(list); return NULL; } - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } return list; @@ -2599,7 +2641,7 @@ PyObject* igraphmodule_strvector_t_to_PyList(igraph_strvector_t *v) { return NULL; } - PyList_SET_ITEM(list, i, item); + PyList_SetItem(list, i, item); /* will not fail */ } /* return the list */ @@ -2688,7 +2730,7 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t(PyObject *it, PyObject *t; while ((t=PyIter_Next(it))) { - if (!PyObject_TypeCheck(t, &igraphmodule_GraphType)) { + if (!PyObject_TypeCheck(t, igraphmodule_GraphType)) { PyErr_SetString(PyExc_TypeError, "iterable argument must contain graphs"); Py_DECREF(t); return 1; @@ -2718,8 +2760,8 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, PyObject *t; int first = 1; - while ((t=PyIter_Next(it))) { - if (!PyObject_TypeCheck(t, &igraphmodule_GraphType)) { + while ((t = PyIter_Next(it))) { + if (!PyObject_TypeCheck(t, igraphmodule_GraphType)) { PyErr_SetString(PyExc_TypeError, "iterable argument must contain graphs"); Py_DECREF(t); return 1; diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 4f0bcdbc9..226496224 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -20,8 +20,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "convert.h" #include "common.h" #include "dfsiter.h" diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 5120456a5..fb8006f34 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -22,8 +22,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "attributes.h" #include "convert.h" #include "edgeobject.h" diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index fbb9c6703..4fc48684f 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -22,8 +22,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "attributes.h" #include "common.h" #include "convert.h" @@ -103,7 +101,7 @@ int igraphmodule_EdgeSeq_init(igraphmodule_EdgeSeqObject *self, PyObject *args, igraph_es_t es; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, - &igraphmodule_GraphType, &g, &esobj)) { + igraphmodule_GraphType, &g, &esobj)) { return -1; } diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index c1c361181..3dc212f20 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -36,7 +36,7 @@ #include "vertexseqobject.h" #include -PyTypeObject igraphmodule_GraphType; +PyTypeObject* igraphmodule_GraphType; #define CREATE_GRAPH_FROM_TYPE(py_graph, c_graph, py_type) { \ py_graph = (igraphmodule_GraphObject*) igraphmodule_Graph_subclass_from_igraph_t( \ @@ -93,6 +93,8 @@ int igraphmodule_Graph_traverse(igraphmodule_GraphObject * self, } } + Py_VISIT(Py_TYPE(self)); + return 0; } @@ -104,9 +106,12 @@ void igraphmodule_Graph_dealloc(igraphmodule_GraphObject * self) { PyObject *r; + RC_DEALLOC("Graph", self); + /* Clear weak references */ - if (self->weakreflist != NULL) + if (self->weakreflist != NULL) { PyObject_ClearWeakRefs((PyObject *) self); + } igraph_destroy(&self->g); @@ -119,9 +124,7 @@ void igraphmodule_Graph_dealloc(igraphmodule_GraphObject * self) igraphmodule_Graph_clear(self); - RC_DEALLOC("Graph", self); - - Py_TYPE(self)->tp_free((PyObject*)self); + PY_FREE_AND_DECREF_TYPE(self); } /** @@ -136,7 +139,6 @@ void igraphmodule_Graph_dealloc(igraphmodule_GraphObject * self) * Throws \c AssertionError in Python if \c vcount is less than or equal to zero. * \return the new \c igraph.Graph object or NULL if an error occurred. * - * \sa igraphmodule_Graph_new * \sa igraph_empty * \sa igraph_create */ @@ -231,7 +233,7 @@ PyObject* igraphmodule_Graph_subclass_from_igraph_t( PyObject* args; PyObject* kwds; - if (!PyType_IsSubtype(type, &igraphmodule_GraphType)) { + if (!PyType_IsSubtype(type, igraphmodule_GraphType)) { PyErr_SetString(PyExc_TypeError, "igraph._igraph.GraphBase expected"); return 0; } @@ -284,7 +286,7 @@ PyObject* igraphmodule_Graph_subclass_from_igraph_t( */ PyObject* igraphmodule_Graph_from_igraph_t(igraph_t *graph) { return igraphmodule_Graph_subclass_from_igraph_t( - &igraphmodule_GraphType, graph + igraphmodule_GraphType, graph ); } @@ -4511,8 +4513,15 @@ PyObject *igraphmodule_Graph_decompose(igraphmodule_GraphObject * self, for (i = 0; i < n; i++) { g = (igraph_t *) VECTOR(components)[i]; CREATE_GRAPH(o, *g); - PyList_SET_ITEM(list, i, (PyObject *) o); - /* reference has been transferred by PyList_SET_ITEM, no need to DECREF + + if (PyList_SetItem(list, i, (PyObject *) o)) { + Py_DECREF(o); + Py_DECREF(list); + igraph_vector_ptr_destroy(&components); + return 0; + } + + /* reference has been transferred by PyList_SetItem, no need to DECREF. * * we mustn't call igraph_destroy here, because it would free the vertices * and the edges as well, but we need them in o->g. So just call free */ @@ -4937,12 +4946,12 @@ PyObject *igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject * for (i = 0; i < no_of_target_nodes; i++) { item = igraphmodule_vector_int_t_to_PyList(&res[i]); if (!item || PyList_SetItem(list, i, item)) { - if (item) { - Py_DECREF(item); + for (j = 0; j < no_of_target_nodes; j++) { + igraph_vector_int_destroy(&res[j]); } - Py_DECREF(list); - for (j = 0; j < no_of_target_nodes; j++) igraph_vector_int_destroy(&res[j]); free(res); + Py_XDECREF(item); + Py_DECREF(list); return NULL; } } @@ -8693,7 +8702,7 @@ PyObject *igraphmodule_Graph_isomorphic(igraphmodule_GraphObject * self, static char *kwlist[] = { "other", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "|O!", kwlist, - &igraphmodule_GraphType, &o)) + igraphmodule_GraphType, &o)) return NULL; if (o == Py_None) other = self; else other = (igraphmodule_GraphObject *) o; @@ -8732,7 +8741,7 @@ PyObject *igraphmodule_Graph_isomorphic_bliss(igraphmodule_GraphObject * self, "return_mapping_21", "sh1", "sh2", "color1", "color2", NULL }; /* TODO: convert igraph_bliss_info_t when needed */ if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!OOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "|O!OOOOOO", kwlist, igraphmodule_GraphType, &o, &return1, &return2, &sho1, &sho2, &color1_o, &color2_o)) return NULL; if (igraphmodule_PyObject_to_bliss_sh_t(sho1, &sh1)) return NULL; @@ -8929,7 +8938,7 @@ PyObject *igraphmodule_Graph_isomorphic_vf2(igraphmodule_GraphObject * self, }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!OOOOOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "|O!OOOOOOOOO", kwlist, igraphmodule_GraphType, &o, &color1_o, &color2_o, &edge_color1_o, &edge_color2_o, &return1, &return2, &callback_fn, &node_compat_fn, &edge_compat_fn)) return NULL; @@ -9064,7 +9073,7 @@ PyObject *igraphmodule_Graph_count_isomorphisms_vf2(igraphmodule_GraphObject *se "node_compat_fn", "edge_compat_fn", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!OOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "|O!OOOOOO", kwlist, igraphmodule_GraphType, &o, &color1_o, &color2_o, &edge_color1_o, &edge_color2_o, &node_compat_fn, &edge_compat_fn)) return NULL; @@ -9157,7 +9166,7 @@ PyObject *igraphmodule_Graph_get_isomorphisms_vf2(igraphmodule_GraphObject *self "edge_color1", "edge_color2", "node_compat_fn", "edge_compat_fn", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "|O!OOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "|O!OOOOOO", kwlist, igraphmodule_GraphType, &o, &color1_o, &color2_o, &edge_color1_o, &edge_color2_o, &node_compat_fn, &edge_compat_fn)) return NULL; @@ -9267,7 +9276,7 @@ PyObject *igraphmodule_Graph_subisomorphic_vf2(igraphmodule_GraphObject * self, NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "O!|OOOOOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "O!|OOOOOOOOO", kwlist, igraphmodule_GraphType, &o, &color1_o, &color2_o, &edge_color1_o, &edge_color2_o, &return1, &return2, &callback_fn, &node_compat_fn, &edge_compat_fn)) return NULL; @@ -9404,7 +9413,7 @@ PyObject *igraphmodule_Graph_count_subisomorphisms_vf2(igraphmodule_GraphObject "edge_color2", "node_compat_fn", "edge_compat_fn", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "O!|OOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "O!|OOOOOO", kwlist, igraphmodule_GraphType, &o, &color1_o, &color2_o, &edge_color1_o, &edge_color2_o, &node_compat_fn, &edge_compat_fn)) return NULL; @@ -9494,7 +9503,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_vf2(igraphmodule_GraphObject *s "edge_color2", "node_compat_fn", "edge_compat_fn", NULL }; if (!PyArg_ParseTupleAndKeywords - (args, kwds, "O!|OOOOOO", kwlist, &igraphmodule_GraphType, &o, + (args, kwds, "O!|OOOOOO", kwlist, igraphmodule_GraphType, &o, &color1_o, &color2_o, &edge_color1_o, &edge_color2_o, &node_compat_fn, &edge_compat_fn)) return NULL; @@ -9590,7 +9599,7 @@ PyObject *igraphmodule_Graph_subisomorphic_lad(igraphmodule_GraphObject * self, "return_mapping", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOfO", kwlist, - &igraphmodule_GraphType, &o, &domains_o, &induced, + igraphmodule_GraphType, &o, &domains_o, &induced, &time_limit, &return_mapping)) return NULL; @@ -9656,7 +9665,7 @@ PyObject *igraphmodule_Graph_get_subisomorphisms_lad( static char *kwlist[] = { "pattern", "domains", "induced", "time_limit", NULL }; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|OOf", kwlist, - &igraphmodule_GraphType, &o, &domains_o, &induced, &time_limit)) + igraphmodule_GraphType, &o, &domains_o, &induced, &time_limit)) return NULL; other=(igraphmodule_GraphObject*)o; @@ -9720,14 +9729,21 @@ PyObject *igraphmodule_Graph_mp_subscript(igraphmodule_GraphObject * self, if (PyTuple_Check(s) && PyTuple_Size(s) >= 2) { /* Adjacency matrix representation */ - PyObject *ri = PyTuple_GET_ITEM(s, 0); - PyObject *ci = PyTuple_GET_ITEM(s, 1); + PyObject *ri = PyTuple_GetItem(s, 0); + PyObject *ci = PyTuple_GetItem(s, 1); PyObject *attr; + if (ri == 0 || ci == 0) { + return 0; + } + if (PyTuple_Size(s) == 2) { attr = 0; } else if (PyTuple_Size(s) == 3) { - attr = PyTuple_GET_ITEM(s, 2); + attr = PyTuple_GetItem(s, 2); + if (attr == 0) { + return 0; + } } else { PyErr_SetString(PyExc_TypeError, "adjacency matrix indexing must use at most three arguments"); return 0; @@ -9773,13 +9789,20 @@ int igraphmodule_Graph_mp_assign_subscript(igraphmodule_GraphObject * self, return -1; } - ri = PyTuple_GET_ITEM(k, 0); - ci = PyTuple_GET_ITEM(k, 1); + ri = PyTuple_GetItem(k, 0); + ci = PyTuple_GetItem(k, 1); + + if (ri == 0 || ci == 0) { + return -1; + } if (PyTuple_Size(k) == 2) { attr = 0; } else if (PyTuple_Size(k) == 3) { - attr = PyTuple_GET_ITEM(k, 2); + attr = PyTuple_GetItem(k, 2); + if (attr == 0) { + return -1; + } } else { PyErr_SetString(PyExc_TypeError, "adjacency matrix indexing must use at most three arguments"); return 0; @@ -9836,7 +9859,7 @@ PyObject *igraphmodule_Graph_difference(igraphmodule_GraphObject * self, igraphmodule_GraphObject *o, *result; igraph_t g; - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { + if (!PyObject_TypeCheck(other, igraphmodule_GraphType)) { Py_INCREF(Py_NotImplemented); return Py_NotImplemented; } @@ -9911,7 +9934,7 @@ PyObject *igraphmodule_Graph_compose(igraphmodule_GraphObject * self, igraphmodule_GraphObject *o, *result; igraph_t g; - if (!PyObject_TypeCheck(other, &igraphmodule_GraphType)) { + if (!PyObject_TypeCheck(other, igraphmodule_GraphType)) { Py_INCREF(Py_NotImplemented); return Py_NotImplemented; } @@ -10955,15 +10978,14 @@ PyObject *igraphmodule_Graph_cliques(igraphmodule_GraphObject * self, for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); - if (!item) { + if (!item || PyList_SetItem(list, i, item)) { for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); } igraph_vector_ptr_destroy_all(&result); + Py_XDECREF(item); Py_DECREF(list); return NULL; - } else { - PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); } @@ -11000,15 +11022,14 @@ PyObject *igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject * self) for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); - if (!item) { + if (!item || PyList_SetItem(list, i, item)) { for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); } igraph_vector_ptr_destroy_all(&result); + Py_XDECREF(item); Py_DECREF(list); return NULL; - } else { - PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); } @@ -11105,15 +11126,14 @@ PyObject *igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject * self, for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); - if (!item) { + if (!item || PyList_SetItem(list, i, item)) { for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); } igraph_vector_ptr_destroy_all(&result); + Py_XDECREF(item); Py_DECREF(list); return NULL; - } else { - PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); } @@ -11196,15 +11216,14 @@ PyObject *igraphmodule_Graph_independent_vertex_sets(igraphmodule_GraphObject for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); - if (!item) { + if (!item || PyList_SetItem(list, i, item)) { for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); } igraph_vector_ptr_destroy_all(&result); + Py_XDECREF(item); Py_DECREF(list); return NULL; - } else { - PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); } @@ -11242,15 +11261,14 @@ PyObject for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); - if (!item) { + if (!item || PyList_SetItem(list, i, item)) { for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); } igraph_vector_ptr_destroy_all(&result); + Py_XDECREF(item); Py_DECREF(list); return NULL; - } else { - PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); } @@ -11289,15 +11307,14 @@ PyObject for (i = 0; i < n; i++) { igraph_vector_int_t *vec = (igraph_vector_int_t *) VECTOR(result)[i]; item = igraphmodule_vector_int_t_to_PyTuple(vec); - if (!item) { + if (!item || PyList_SetItem(list, i, item)) { for (j = i; j < n; j++) { igraph_vector_int_destroy((igraph_vector_int_t *) VECTOR(result)[j]); } igraph_vector_ptr_destroy_all(&result); + Py_XDECREF(item); Py_DECREF(list); return NULL; - } else { - PyList_SET_ITEM(list, i, item); } igraph_vector_int_destroy(vec); } @@ -16312,111 +16329,52 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { {NULL} }; -/** \ingroup python_interface_graph - * This structure is the collection of functions necessary to implement - * the graph as a mapping (i.e. to allow the retrieval and setting of - * igraph attributes in Python as if it were of a Python mapping type) - */ -PyMappingMethods igraphmodule_Graph_as_mapping = { - /* __len__ function intentionally left unimplemented */ - 0, - /* returns an attribute by name or returns part of the adjacency matrix */ - (binaryfunc) igraphmodule_Graph_mp_subscript, - /* sets an attribute by name or sets part of the adjacency matrix */ - (objobjargproc) igraphmodule_Graph_mp_assign_subscript -}; - -/** \ingroup python_interface - * \brief Collection of methods to allow numeric operators to be used on the graph +/** + * \ingroup python_interface_graph + * Member table for the \c igraph._igraph.GraphBase object */ -PyNumberMethods igraphmodule_Graph_as_number = { - 0, /* nb_add */ - 0, /*nb_subtract */ - 0, /*nb_multiply */ - 0, /*nb_remainder */ - 0, /*nb_divmod */ - 0, /*nb_power */ - 0, /*nb_negative */ - 0, /*nb_positive */ - 0, /*nb_absolute */ - 0, /*nb_nonzero (2.x) / nb_bool (3.x) */ - (unaryfunc) igraphmodule_Graph_complementer_op, /*nb_invert */ - 0, /*nb_lshift */ - 0, /*nb_rshift */ - 0, /*nb_and */ - 0, /*nb_xor */ - 0, /*nb_or */ - 0, /*nb_int */ - 0, /*nb_long (2.x) / nb_reserved (3.x)*/ - 0, /*nb_float */ - 0, /*nb_inplace_add */ - 0, /*nb_inplace_subtract */ - 0, /*nb_inplace_multiply */ - 0, /*nb_inplace_remainder */ - 0, /*nb_inplace_power */ - 0, /*nb_inplace_lshift */ - 0, /*nb_inplace_rshift */ - 0, /*nb_inplace_and */ - 0, /*nb_inplace_xor */ - 0, /*nb_inplace_or */ - 0, /*nb_floor_divide */ - 0, /*nb_true_divide */ - 0, /*nb_inplace_floor_divide */ - 0, /*nb_inplace_true_divide */ - 0, /*nb_index */ +PyMemberDef igraphmodule_Graph_members[] = { + {"__weaklistoffset__", T_PYSSIZET, offsetof(igraphmodule_GraphObject, weakreflist), READONLY}, + { 0 } }; -/** \ingroup python_interface_graph - * Python type object referencing the methods Python calls when it performs various operations on an igraph (creating, printing and so on) - */ -PyTypeObject igraphmodule_GraphType = { - PyVarObject_HEAD_INIT(0, 0) - "igraph._igraph.GraphBase", /* tp_name */ - sizeof(igraphmodule_GraphObject), /* tp_basicsize */ - 0, /* tp_itemsize */ - (destructor) igraphmodule_Graph_dealloc, /* tp_dealloc */ - 0, /* tp_print */ - 0, /* tp_getattr */ - 0, /* tp_setattr */ - 0, /* tp_compare (2.x) / tp_reserved (3.x) */ - 0, /* tp_repr */ - &igraphmodule_Graph_as_number, /* tp_as_number */ - 0, /* tp_as_sequence */ - &igraphmodule_Graph_as_mapping, /* tp_as_mapping */ -#ifndef PYPY_VERSION - (hashfunc) PyObject_HashNotImplemented, /* tp_hash */ -#else - /* PyObject_HashNotImplemented raises an exception but it is not handled - * properly by PyPy so we don't use it */ - 0, /* tp_hash */ -#endif - 0, /* tp_call */ - (reprfunc) igraphmodule_Graph_str, /* tp_str */ - 0, /* tp_getattro */ - 0, /* tp_setattro */ - 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* tp_flags */ +PyDoc_STRVAR( + igraphmodule_Graph_doc, "Low-level representation of a graph.\n\n" "Don't use it directly, use L{igraph.Graph} instead.\n\n" - "@deffield ref: Reference", /* tp_doc */ - (traverseproc) igraphmodule_Graph_traverse, /* tp_traverse */ - (inquiry) igraphmodule_Graph_clear, /* tp_clear */ - 0, /* tp_richcompare */ - offsetof(igraphmodule_GraphObject, weakreflist), /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - igraphmodule_Graph_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc) igraphmodule_Graph_init, /* tp_init */ - 0, /* tp_alloc */ - PyType_GenericNew, /* tp_new */ - 0, /* tp_free */ -}; + "@deffield ref: Reference" /* tp_doc */ +); + +int igraphmodule_Graph_register_type() { + PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_Graph_init }, + { Py_tp_dealloc, igraphmodule_Graph_dealloc }, + { Py_tp_members, igraphmodule_Graph_members }, + { Py_tp_methods, igraphmodule_Graph_methods }, + { Py_tp_hash, PyObject_HashNotImplemented }, + { Py_tp_traverse, igraphmodule_Graph_traverse }, + { Py_tp_clear, igraphmodule_Graph_clear }, + { Py_tp_str, igraphmodule_Graph_str }, + { Py_tp_doc, (void*) igraphmodule_Graph_doc }, + + { Py_nb_invert, igraphmodule_Graph_complementer_op }, + + { Py_mp_subscript, igraphmodule_Graph_mp_subscript }, + { Py_mp_ass_subscript, igraphmodule_Graph_mp_assign_subscript }, + + { 0 } + }; + + PyType_Spec spec = { + "igraph._igraph.GraphBase", /* name */ + sizeof(igraphmodule_GraphObject), /* basicsize */ + 0, /* itemsize */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /* flags */ + slots, /* slots */ + }; + + igraphmodule_GraphType = (PyTypeObject*) PyType_FromSpec(&spec); + return igraphmodule_GraphType == 0; +} #undef CREATE_GRAPH diff --git a/src/_igraph/graphobject.h b/src/_igraph/graphobject.h index 67521ad2c..164b875cb 100644 --- a/src/_igraph/graphobject.h +++ b/src/_igraph/graphobject.h @@ -29,7 +29,7 @@ #include "structmember.h" #include "common.h" -extern PyTypeObject igraphmodule_GraphType; +extern PyTypeObject* igraphmodule_GraphType; /** * \ingroup python_interface @@ -50,182 +50,13 @@ typedef struct PyObject* weakreflist; } igraphmodule_GraphObject; -PyObject* igraphmodule_Graph_new(PyTypeObject *type, PyObject *args, PyObject *kwds); -int igraphmodule_Graph_clear(igraphmodule_GraphObject *self); -int igraphmodule_Graph_traverse(igraphmodule_GraphObject *self, visitproc visit, void *arg); -void igraphmodule_Graph_dealloc(igraphmodule_GraphObject* self); -int igraphmodule_Graph_init(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); +int igraphmodule_Graph_register_type(void); + PyObject* igraphmodule_Graph_subclass_from_igraph_t(PyTypeObject* type, igraph_t *graph); PyObject* igraphmodule_Graph_from_igraph_t(igraph_t *graph); -PyObject* igraphmodule_Graph_str(igraphmodule_GraphObject *self); - -PyObject* igraphmodule_Graph_vcount(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_ecount(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_is_dag(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_is_directed(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_is_simple(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_add_vertices(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_delete_vertices(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_add_edges(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_delete_edges(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_degree(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_is_loop(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_count_multiple(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_neighbors(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_successors(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_predecessors(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_get_eid(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_Adjacency(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Asymmetric_Preference(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Atlas(PyTypeObject *type, PyObject *args); -PyObject* igraphmodule_Graph_Barabasi(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Degree_Sequence(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Establishment(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Erdos_Renyi(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Famous(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Forest_Fire(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Full_Citation(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Full(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_GRG(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Growing_Random(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Isoclass(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Lattice(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_LCF(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Preference(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Recent_Degree(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Ring(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_SBM(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Star(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Tree(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Tree_Game(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Watts_Strogatz(PyTypeObject *type, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_is_connected(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_are_connected(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_adjacency_spectral_embedding(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_articulation_points(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_average_path_length(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_betweenness(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_bibcoupling(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_closeness(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_clusters(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_cocitation(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_constraint(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_copy(igraphmodule_GraphObject *self); -PyObject* igraphmodule_Graph_decompose(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_density(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_diameter(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_edge_betweenness(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_eigen_adjacency(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_get_shortest_paths(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_get_all_shortest_paths(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_maxdegree(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_pagerank(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_path_length_hist(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_reciprocity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_rewire(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_shortest_paths(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_spanning_tree(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_simplify(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_subcomponent(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_subgraph(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_transitivity_undirected(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_transitivity_local_undirected(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_scan1(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_layout_circle(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_sphere(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_random(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_random_3d(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_kamada_kawai_3d(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_drl(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_fruchterman_reingold(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_fruchterman_reingold_3d(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_layout_reingold_tilford(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_get_adjacency(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_get_edgelist(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_to_undirected(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_to_directed(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_laplacian(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph_Read_DIMACS(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Read_Edgelist(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Read_GML(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Read_Ncol(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Read_Lgl(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Read_Pajek(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_Read_GraphML(PyTypeObject *type, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_dimacs(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_dot(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_edgelist(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_ncol(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_lgl(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_gml(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_write_graphml(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_isoclass(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_isomorphic(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_count_isomorphisms(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_get_isomorphisms(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_subisomorphic(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_count_subisomorphisms(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_get_subisomorphisms(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); - -Py_ssize_t igraphmodule_Graph_attribute_count(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_get_attribute(igraphmodule_GraphObject* self, PyObject* s); -int igraphmodule_Graph_set_attribute(igraphmodule_GraphObject* self, PyObject* k, PyObject* v); PyObject* igraphmodule_Graph_attributes(igraphmodule_GraphObject* self); PyObject* igraphmodule_Graph_vertex_attributes(igraphmodule_GraphObject* self); PyObject* igraphmodule_Graph_edge_attributes(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_get_vertices(igraphmodule_GraphObject* self, void* closure); -PyObject* igraphmodule_Graph_get_edges(igraphmodule_GraphObject* self, void* closure); - -PyObject* igraphmodule_Graph_complementer(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_complementer_op(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_compose(igraphmodule_GraphObject* self, PyObject* other); -PyObject* igraphmodule_Graph_difference(igraphmodule_GraphObject* self, PyObject* other); -PyObject* igraphmodule_Graph_disjoint_union(igraphmodule_GraphObject* self, PyObject* other); - -PyObject* igraphmodule_Graph_bfs(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_bfsiter(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); - -PyObject* igraphmodule_Graph_maxflow(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_maxflow_value(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_mincut(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_mincut_value(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); - -PyObject* igraphmodule_Graph_cliques(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_maximal_cliques(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_largest_cliques(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_clique_number(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_independent_sets(igraphmodule_GraphObject* self, PyObject* args, PyObject* kwds); -PyObject* igraphmodule_Graph_maximal_independent_sets(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_largest_independent_sets(igraphmodule_GraphObject* self); -PyObject* igraphmodule_Graph_independence_number(igraphmodule_GraphObject* self); - -PyObject* igraphmodule_Graph_community_edge_betweenness(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_community_fastgreedy(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject *igraphmodule_Graph_community_infomap(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_community_label_propagation(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_community_leading_eigenvector(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject *igraphmodule_Graph_community_multilevel(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject *igraphmodule_Graph_community_optimal_modularity(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_community_spinglass(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_community_walktrap(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_modularity(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph_leiden(igraphmodule_GraphObject* self, PyObject *args, PyObject *kwds); - -PyObject *igraphmodule_Graph_is_bipartite(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - -PyObject* igraphmodule_Graph___graph_as_cobject__(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); -PyObject* igraphmodule_Graph___register_destructor__(igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds); - #endif diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index d4dfa1e7d..7725b1622 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -843,13 +843,12 @@ PyObject* PyInit__igraph(void) igraphmodule_DFSIter_register_type() || igraphmodule_Edge_register_type() || igraphmodule_EdgeSeq_register_type() || + igraphmodule_Graph_register_type() || igraphmodule_Vertex_register_type() || igraphmodule_VertexSeq_register_type() ) { INITERROR; } - if (PyType_Ready(&igraphmodule_GraphType) < 0) - INITERROR; /* Initialize the core module */ m = PyModule_Create(&moduledef); @@ -860,7 +859,7 @@ PyObject* PyInit__igraph(void) igraphmodule_init_rng(m); /* Add the types to the core module */ - PyModule_AddObject(m, "GraphBase", (PyObject*)&igraphmodule_GraphType); + PyModule_AddObject(m, "GraphBase", (PyObject*)igraphmodule_GraphType); PyModule_AddObject(m, "BFSIter", (PyObject*)igraphmodule_BFSIterType); PyModule_AddObject(m, "DFSIter", (PyObject*)igraphmodule_DFSIterType); PyModule_AddObject(m, "ARPACKOptions", (PyObject*)igraphmodule_ARPACKOptionsType); diff --git a/src/_igraph/operators.c b/src/_igraph/operators.c index 0ad5a0293..52be31514 100644 --- a/src/_igraph/operators.c +++ b/src/_igraph/operators.c @@ -154,16 +154,26 @@ PyObject *igraphmodule__union(PyObject *self, Py_ssize_t no_of_edges = igraph_ecount(VECTOR(gs)[i]); igraph_vector_int_t *map = VECTOR(edgemaps)[i]; PyObject *emi = PyList_New(no_of_edges); - for (j = 0; j < no_of_edges; j++) { - PyObject *dest = igraphmodule_integer_t_to_PyObject(VECTOR(*map)[j]); - if (!dest) { - Py_DECREF(emi); - Py_DECREF(em_list); - return NULL; + if (emi) { + for (j = 0; j < no_of_edges; j++) { + PyObject *dest = igraphmodule_integer_t_to_PyObject(VECTOR(*map)[j]); + if (!dest || PyList_SetItem(emi, j, dest)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_ptr_destroy(&edgemaps); + Py_XDECREF(dest); + Py_DECREF(emi); + Py_DECREF(em_list); + return NULL; + } } - PyList_SET_ITEM(emi, j, dest); } - PyList_SET_ITEM(em_list, i, emi); + if (!emi || PyList_SetItem(em_list, i, emi)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_ptr_destroy(&edgemaps); + Py_XDECREF(emi); + Py_DECREF(em_list); + return NULL; + } } igraph_vector_ptr_destroy(&edgemaps); } else { @@ -266,16 +276,26 @@ PyObject *igraphmodule__intersection(PyObject *self, Py_ssize_t no_of_edges = igraph_ecount(VECTOR(gs)[i]); igraph_vector_int_t *map = VECTOR(edgemaps)[i]; PyObject *emi = PyList_New(no_of_edges); - for (j = 0; j < no_of_edges; j++) { - PyObject *dest = igraphmodule_integer_t_to_PyObject(VECTOR(*map)[j]); - if (!dest) { - Py_DECREF(emi); - Py_DECREF(em_list); - return NULL; + if (emi) { + for (j = 0; j < no_of_edges; j++) { + PyObject *dest = igraphmodule_integer_t_to_PyObject(VECTOR(*map)[j]); + if (!dest || PyList_SetItem(emi, j, dest)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_ptr_destroy(&edgemaps); + Py_XDECREF(dest); + Py_DECREF(emi); + Py_DECREF(em_list); + return NULL; + } } - PyList_SET_ITEM(emi, j, dest); } - PyList_SET_ITEM(em_list, i, emi); + if (!emi || PyList_SetItem(em_list, i, emi)) { + igraph_vector_ptr_destroy(&gs); + igraph_vector_ptr_destroy(&edgemaps); + Py_XDECREF(emi); + Py_DECREF(em_list); + return NULL; + } } igraph_vector_ptr_destroy(&edgemaps); diff --git a/src/_igraph/preamble.h b/src/_igraph/preamble.h index 218b304ca..fa427ccac 100644 --- a/src/_igraph/preamble.h +++ b/src/_igraph/preamble.h @@ -23,6 +23,10 @@ #ifndef PYTHON_IGRAPH_PREAMBLE_H #define PYTHON_IGRAPH_PREAMBLE_H +#ifndef PY_IGRAPH_ALLOW_ENTIRE_PYTHON_API +# define Py_LIMITED_API 0x03060100 +#endif + #define PY_SSIZE_T_CLEAN #include diff --git a/src/_igraph/pyhelpers.c b/src/_igraph/pyhelpers.c index a51b60ca7..5cf0e075e 100644 --- a/src/_igraph/pyhelpers.c +++ b/src/_igraph/pyhelpers.c @@ -66,12 +66,17 @@ PyObject* igraphmodule_PyList_NewFill(Py_ssize_t len, PyObject* item) { Py_ssize_t i; PyObject* result = PyList_New(len); - if (result == 0) + if (result == 0) { return 0; + } for (i = 0; i < len; i++) { Py_INCREF(item); - PyList_SET_ITEM(result, i, item); /* reference to item stolen */ + if (PyList_SetItem(result, i, item)) { + Py_DECREF(item); + Py_DECREF(result); + return 0; + } } return result; @@ -161,14 +166,21 @@ char* PyUnicode_CopyAsString(PyObject* string) { bytes = PyUnicode_AsUTF8String(string); } - if (bytes == 0) + if (bytes == 0) { + return 0; + } + + result = PyBytes_AsString(bytes); + if (result == 0) { + Py_DECREF(bytes); return 0; - - result = strdup(PyBytes_AS_STRING(bytes)); + } Py_DECREF(bytes); - if (result == 0) + result = strdup(result); + if (result == 0) { PyErr_NoMemory(); + } return result; } diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index a51578fdc..df8897cb4 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -22,8 +22,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "attributes.h" #include "convert.h" #include "edgeobject.h" diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 02160beb5..2862d3e3d 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -22,8 +22,6 @@ */ -#define Py_LIMITED_API 0x03060100 - #include "attributes.h" #include "common.h" #include "convert.h" @@ -100,7 +98,7 @@ int igraphmodule_VertexSeq_init(igraphmodule_VertexSeqObject *self, igraph_vs_t vs; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, - &igraphmodule_GraphType, &g, &vsobj)) + igraphmodule_GraphType, &g, &vsobj)) return -1; if (vsobj == Py_None) { diff --git a/src/igraph/utils.py b/src/igraph/utils.py index e1bce6541..d195a6263 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -62,10 +62,6 @@ def numpy_to_contiguous_memoryview(obj): from numpy import int32, int64, require from igraph._igraph import INTEGER_SIZE - # TODO: we used to export to double, which is only dependent on the - # architecture. Now with integers and a compile-time flag, we have - # to figure out what is the integer bitness of the underlying C core. - # Think of how to do that, for now default to 64 bit ints! if INTEGER_SIZE == 64: dtype = int64 elif INTEGER_SIZE == 32: diff --git a/tests/test_basic.py b/tests/test_basic.py index e21e67905..db7146d30 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,10 +1,15 @@ +import gc +import sys import unittest import warnings +from contextlib import contextmanager from functools import partial from igraph import ( ALL, + Edge, + EdgeSeq, Graph, IN, InternalError, @@ -12,7 +17,10 @@ is_graphical, is_graphical_degree_sequence, Matrix, + Vertex, + VertexSeq ) +from igraph._igraph import EdgeSeq as _EdgeSeq, VertexSeq as _VertexSeq try: import numpy as np @@ -721,7 +729,7 @@ def __init__(self, *args, **kwds): self.init_called = True def __new__(cls, *args, **kwds): - result = Graph.__new__(cls, *args, **kwds) + result = Graph.__new__(cls) result.new_called = True return result @@ -759,6 +767,58 @@ def testCallingClassMethodInSuperclass(self): self.assertTrue(getattr(g, "adjacency_called", True)) +@contextmanager +def assert_reference_not_leaked(case, *args): + gc.collect() + refs_before = [sys.getrefcount(obj) for obj in args] + try: + yield + finally: + gc.collect() + refs_after = [sys.getrefcount(obj) for obj in args] + case.assertListEqual(refs_before, refs_after) + + +class ReferenceCountTests(unittest.TestCase): + def testEdgeReferenceCounting(self): + with assert_reference_not_leaked(self, Edge, EdgeSeq, _EdgeSeq): + g = Graph.Tree(3, 2) + edge = g.es[1] + del edge, g + + def testEdgeSeqReferenceCounting(self): + with assert_reference_not_leaked(self, Edge, EdgeSeq, _EdgeSeq): + g = Graph.Tree(3, 2) + es = g.es + es2 = EdgeSeq(g) + del es, es2, g + + def testGraphReferenceCounting(self): + with assert_reference_not_leaked(self, Graph, InheritedGraph): + g = Graph.Tree(3, 2) + self.assertTrue(gc.is_tracked(g)) + del g + + def testInheritedGraphReferenceCounting(self): + with assert_reference_not_leaked(self, Graph, InheritedGraph): + g = InheritedGraph.Tree(3, 2) + self.assertTrue(gc.is_tracked(g)) + del g + + def testVertexReferenceCounting(self): + with assert_reference_not_leaked(self, Vertex, VertexSeq, _VertexSeq): + g = Graph.Tree(3, 2) + vertex = g.vs[2] + del vertex, g + + def testVertexSeqReferenceCounting(self): + with assert_reference_not_leaked(self, Vertex, VertexSeq, _VertexSeq): + g = Graph.Tree(3, 2) + vs = g.vs + vs2 = VertexSeq(g) + del vs2, vs, g + + def suite(): basic_suite = unittest.makeSuite(BasicTests) datatype_suite = unittest.makeSuite(DatatypeTests) @@ -766,6 +826,7 @@ def suite(): graph_tuple_list_suite = unittest.makeSuite(GraphTupleListTests) degree_sequence_suite = unittest.makeSuite(DegreeSequenceTests) inheritance_suite = unittest.makeSuite(InheritanceTests) + refcount_suite = unittest.makeSuite(ReferenceCountTests) return unittest.TestSuite( [ basic_suite, @@ -774,6 +835,7 @@ def suite(): graph_tuple_list_suite, degree_sequence_suite, inheritance_suite, + refcount_suite ] ) From a53fc18a1b61834da5b7af169c5aef2d28958a77 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 14:58:47 +0200 Subject: [PATCH 0411/1681] chore: updated cibuildwheel to 2.1.1 --- .github/workflows/build.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7d0354422..7fec758fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - CIBW_SKIP: "cp27-* pp27-* cp35-*" jobs: build_wheels: @@ -26,7 +25,7 @@ jobs: python-version: '3.8' - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -50,7 +49,7 @@ jobs: uses: docker/setup-qemu-action@v1 - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -88,7 +87,7 @@ jobs: brew install ninja autoconf automake libtool cmake - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_ARCHS_MACOS: "x86_64 arm64" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -145,7 +144,7 @@ jobs: shell: cmd - name: Build wheels - uses: joerick/cibuildwheel@v1.11.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" From a2122bfd1f717fd5ec0aff7db77b60cf9e41226a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 15:28:01 +0200 Subject: [PATCH 0412/1681] ci: skip testing on Python 3.10 until NumPy / SciPy / Pandas publish wheels --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7fec758fe..cf561c68e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,8 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" + # skip testing on Python 3.10 until NumPy/SciPy/Pandas publish wheels + CIBW_TEST_SKIP: "cp310-*" jobs: build_wheels: @@ -148,12 +150,12 @@ jobs: env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" + CIBW_TEST_COMMAND: "cd /d {project} && python -m pytest tests" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - CIBW_TEST_COMMAND: "cd /d {project} && python -m unittest -vvv" - uses: actions/upload-artifact@v2 with: From eeec9887e0531febbecfa28fee69292b3d38a14f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 16:07:42 +0200 Subject: [PATCH 0413/1681] refactor: added __init__ implementation to Vertex and Edge --- src/_igraph/convert.c | 15 ++++++++---- src/_igraph/edgeobject.c | 45 +++++++++++++++++++++-------------- src/_igraph/edgeseqobject.c | 4 +--- src/_igraph/vertexobject.c | 45 +++++++++++++++++++++-------------- src/_igraph/vertexseqobject.c | 3 +-- 5 files changed, 66 insertions(+), 46 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index b790689bd..192ff0232 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2789,8 +2789,9 @@ int igraphmodule_append_PyIter_of_graphs_to_vector_ptr_t_with_type(PyObject *it, * \return 0 if everything was OK, 1 otherwise */ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *graph) { - if (o == Py_None || o == 0) { - *vid = 0; + if (o == 0) { + PyErr_SetString(PyExc_TypeError, "only numbers, strings or igraph.Vertex objects can be converted to vertex IDs"); + return 1; } else if (PyLong_Check(o)) { /* Single vertex ID */ if (igraphmodule_PyObject_to_integer_t(o, vid)) { @@ -3004,12 +3005,16 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g int retval; igraph_integer_t vid1, vid2; - if (o == Py_None || o == 0) { - *eid = 0; + if (!o) { + PyErr_SetString(PyExc_TypeError, + "only numbers, igraph.Edge objects or tuples of vertex IDs can be " + "converted to edge IDs"); + return 1; } else if (PyLong_Check(o)) { /* Single edge ID */ - if (igraphmodule_PyObject_to_integer_t(o, eid)) + if (igraphmodule_PyObject_to_integer_t(o, eid)) { return 1; + } } else if (igraphmodule_Edge_Check(o)) { /* Single edge ID from Edge object */ igraphmodule_EdgeObject *eo = (igraphmodule_EdgeObject*)o; diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index fb8006f34..3d97d61c0 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -99,27 +99,36 @@ int igraphmodule_Edge_Validate(PyObject* obj) { * (or they might even get invalidated). */ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { - igraphmodule_EdgeObject* self; - - self = (igraphmodule_EdgeObject*) PyType_GenericNew(igraphmodule_EdgeType, 0, 0); - - if (self) { - RC_ALLOC("Edge", self); - Py_INCREF(gref); - self->gref = gref; - self->idx = idx; - self->hash = -1; - } - - return (PyObject*)self; + return PyObject_CallFunction(igraphmodule_EdgeType, "On", gref, (Py_ssize_t) idx); } /** * \ingroup python_interface_edge - * \brief Clears the edge's subobject (before deallocation) + * \brief Initialize a new edge object for a given graph + * \return the initialized PyObject */ -static int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { - Py_CLEAR(self->gref); +static int igraphmodule_Edge_init(igraphmodule_EdgeObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "graph", "eid", NULL }; + PyObject *g, *index_o = Py_None; + igraph_integer_t eid; + igraph_t *graph; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, + igraphmodule_GraphType, &g, &index_o)) { + return -1; + } + + graph = &((igraphmodule_GraphObject*)g)->g; + + if (igraphmodule_PyObject_to_eid(index_o, &eid, graph)) { + return -1; + } + + Py_INCREF(g); + self->gref = (igraphmodule_GraphObject*)g; + self->idx = eid; + self->hash = -1; + return 0; } @@ -129,7 +138,7 @@ static int igraphmodule_Edge_clear(igraphmodule_EdgeObject *self) { */ static void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { RC_DEALLOC("Edge", self); - igraphmodule_Edge_clear(self); + Py_CLEAR(self->gref); PY_FREE_AND_DECREF_TYPE(self); } @@ -717,8 +726,8 @@ PyDoc_STRVAR( int igraphmodule_Edge_register_type() { PyType_Slot slots[] = { + { Py_tp_init, igraphmodule_Edge_init }, { Py_tp_dealloc, igraphmodule_Edge_dealloc }, - { Py_tp_clear, igraphmodule_Edge_clear }, { Py_tp_hash, igraphmodule_Edge_hash }, { Py_tp_repr, igraphmodule_Edge_repr }, { Py_tp_richcompare, igraphmodule_Edge_richcompare }, diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index 4fc48684f..bd94219d7 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -89,7 +89,6 @@ igraphmodule_EdgeSeq_copy(igraphmodule_EdgeSeqObject* o) { return copy; } - /** * \ingroup python_interface_edgeseq * \brief Initialize a new edge sequence object for a given graph @@ -162,10 +161,9 @@ void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self) { if (self->gref) { igraph_es_destroy(&self->es); - Py_DECREF(self->gref); - self->gref=0; } + Py_CLEAR(self->gref); PY_FREE_AND_DECREF_TYPE(self); } diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index df8897cb4..50ee8034b 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -99,27 +99,36 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { * (or they might even get invalidated). */ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { - igraphmodule_VertexObject* self; - - self = (igraphmodule_VertexObject*) PyType_GenericNew(igraphmodule_VertexType, 0, 0); - - if (self) { - RC_ALLOC("Vertex", self); - Py_INCREF(gref); - self->gref = gref; - self->idx = idx; - self->hash = -1; - } - - return (PyObject*)self; + return PyObject_CallFunction(igraphmodule_VertexType, "On", gref, (Py_ssize_t) idx); } /** * \ingroup python_interface_vertex - * \brief Clears the vertex's subobject (before deallocation) + * \brief Initialize a new vertex object for a given graph + * \return the initialized PyObject */ -static int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { - Py_CLEAR(self->gref); +static int igraphmodule_Vertex_init(igraphmodule_EdgeObject *self, PyObject *args, PyObject *kwds) { + static char *kwlist[] = { "graph", "vid", NULL }; + PyObject *g, *index_o = Py_None; + igraph_integer_t vid; + igraph_t *graph; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!|O", kwlist, + igraphmodule_GraphType, &g, &index_o)) { + return -1; + } + + graph = &((igraphmodule_GraphObject*)g)->g; + + if (igraphmodule_PyObject_to_vid(index_o, &vid, graph)) { + return -1; + } + + Py_INCREF(g); + self->gref = (igraphmodule_GraphObject*)g; + self->idx = vid; + self->hash = -1; + return 0; } @@ -129,7 +138,7 @@ static int igraphmodule_Vertex_clear(igraphmodule_VertexObject *self) { */ static void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { RC_DEALLOC("Vertex", self); - igraphmodule_Vertex_clear(self); + Py_CLEAR(self->gref); PY_FREE_AND_DECREF_TYPE(self); } @@ -857,7 +866,7 @@ PyDoc_STRVAR( int igraphmodule_Vertex_register_type() { PyType_Slot slots[] = { - { Py_tp_clear, igraphmodule_Vertex_clear }, + { Py_tp_init, igraphmodule_Vertex_init }, { Py_tp_dealloc, igraphmodule_Vertex_dealloc }, { Py_tp_hash, igraphmodule_Vertex_hash }, { Py_tp_repr, igraphmodule_Vertex_repr }, diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 2862d3e3d..947b699f0 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -156,10 +156,9 @@ void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self) { if (self->gref) { igraph_vs_destroy(&self->vs); - Py_DECREF(self->gref); - self->gref=0; } + Py_CLEAR(self->gref); PY_FREE_AND_DECREF_TYPE(self); } From 0034011f6aa47ff9d05c6465d54796dca1001492 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 16:34:20 +0200 Subject: [PATCH 0414/1681] fix: worked around refcounting bug in Python <3.8 --- src/_igraph/arpackobject.c | 2 +- src/_igraph/bfsiter.c | 2 +- src/_igraph/dfsiter.c | 2 +- src/_igraph/edgeobject.c | 4 ++-- src/_igraph/edgeseqobject.c | 2 +- src/_igraph/graphobject.c | 2 +- src/_igraph/pyhelpers.h | 7 +++++-- src/_igraph/vertexobject.c | 4 ++-- src/_igraph/vertexseqobject.c | 2 +- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/_igraph/arpackobject.c b/src/_igraph/arpackobject.c index 86d07f307..047dbba01 100644 --- a/src/_igraph/arpackobject.c +++ b/src/_igraph/arpackobject.c @@ -52,7 +52,7 @@ int igraphmodule_ARPACKOptions_init(igraphmodule_ARPACKOptionsObject *self, PyOb */ static void igraphmodule_ARPACKOptions_dealloc(igraphmodule_ARPACKOptionsObject* self) { RC_DEALLOC("ARPACKOptions", self); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_ARPACKOptionsType); } /** \ingroup python_interface_arpack diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index a250bbedd..03d5308e3 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -152,7 +152,7 @@ static void igraphmodule_BFSIter_dealloc(igraphmodule_BFSIterObject* self) { igraphmodule_BFSIter_clear(self); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_BFSIterType); } static PyObject* igraphmodule_BFSIter_iter(igraphmodule_BFSIterObject* self) { diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 226496224..12fb9a503 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -151,7 +151,7 @@ static int igraphmodule_DFSIter_clear(igraphmodule_DFSIterObject *self) { static void igraphmodule_DFSIter_dealloc(igraphmodule_DFSIterObject* self) { RC_DEALLOC("DFSIter", self); igraphmodule_DFSIter_clear(self); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_DFSIterType); } static PyObject* igraphmodule_DFSIter_iter(igraphmodule_DFSIterObject* self) { diff --git a/src/_igraph/edgeobject.c b/src/_igraph/edgeobject.c index 3d97d61c0..56304434e 100644 --- a/src/_igraph/edgeobject.c +++ b/src/_igraph/edgeobject.c @@ -99,7 +99,7 @@ int igraphmodule_Edge_Validate(PyObject* obj) { * (or they might even get invalidated). */ PyObject* igraphmodule_Edge_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { - return PyObject_CallFunction(igraphmodule_EdgeType, "On", gref, (Py_ssize_t) idx); + return PyObject_CallFunction((PyObject*) igraphmodule_EdgeType, "On", gref, (Py_ssize_t) idx); } /** @@ -139,7 +139,7 @@ static int igraphmodule_Edge_init(igraphmodule_EdgeObject *self, PyObject *args, static void igraphmodule_Edge_dealloc(igraphmodule_EdgeObject* self) { RC_DEALLOC("Edge", self); Py_CLEAR(self->gref); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_EdgeType); } /** \ingroup python_interface_edge diff --git a/src/_igraph/edgeseqobject.c b/src/_igraph/edgeseqobject.c index bd94219d7..64f3f0372 100644 --- a/src/_igraph/edgeseqobject.c +++ b/src/_igraph/edgeseqobject.c @@ -164,7 +164,7 @@ void igraphmodule_EdgeSeq_dealloc(igraphmodule_EdgeSeqObject* self) { } Py_CLEAR(self->gref); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_EdgeSeqType); } /** diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 3dc212f20..af80fa817 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -124,7 +124,7 @@ void igraphmodule_Graph_dealloc(igraphmodule_GraphObject * self) igraphmodule_Graph_clear(self); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_GraphType); } /** diff --git a/src/_igraph/pyhelpers.h b/src/_igraph/pyhelpers.h index 56fd4d399..6189a34c8 100644 --- a/src/_igraph/pyhelpers.h +++ b/src/_igraph/pyhelpers.h @@ -50,15 +50,18 @@ char* PyUnicode_CopyAsString(PyObject* string); /* Calling Py_DECREF() on heap-allocated types in tp_dealloc was not needed * before Python 3.8 (see Python issue 35810) */ #if PY_VERSION_HEX >= 0x03080000 - #define PY_FREE_AND_DECREF_TYPE(obj) { \ + #define PY_FREE_AND_DECREF_TYPE(obj, base_type) { \ PyTypeObject* _tp = Py_TYPE(obj); \ ((freefunc)PyType_GetSlot(_tp, Py_tp_free))(obj); \ Py_DECREF(_tp); \ } #else - #define PY_FREE_AND_DECREF_TYPE(obj) { \ + #define PY_FREE_AND_DECREF_TYPE(obj, base_type) { \ PyTypeObject* _tp = Py_TYPE(obj); \ ((freefunc)PyType_GetSlot(_tp, Py_tp_free))(obj); \ + if (_tp == base_type) { \ + Py_DECREF(_tp); \ + } \ } #endif diff --git a/src/_igraph/vertexobject.c b/src/_igraph/vertexobject.c index 50ee8034b..c1bd338a1 100644 --- a/src/_igraph/vertexobject.c +++ b/src/_igraph/vertexobject.c @@ -99,7 +99,7 @@ int igraphmodule_Vertex_Validate(PyObject* obj) { * (or they might even get invalidated). */ PyObject* igraphmodule_Vertex_New(igraphmodule_GraphObject *gref, igraph_integer_t idx) { - return PyObject_CallFunction(igraphmodule_VertexType, "On", gref, (Py_ssize_t) idx); + return PyObject_CallFunction((PyObject*) igraphmodule_VertexType, "On", gref, (Py_ssize_t) idx); } /** @@ -139,7 +139,7 @@ static int igraphmodule_Vertex_init(igraphmodule_EdgeObject *self, PyObject *arg static void igraphmodule_Vertex_dealloc(igraphmodule_VertexObject* self) { RC_DEALLOC("Vertex", self); Py_CLEAR(self->gref); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_VertexType); } /** \ingroup python_interface_vertex diff --git a/src/_igraph/vertexseqobject.c b/src/_igraph/vertexseqobject.c index 947b699f0..819d18046 100644 --- a/src/_igraph/vertexseqobject.c +++ b/src/_igraph/vertexseqobject.c @@ -159,7 +159,7 @@ void igraphmodule_VertexSeq_dealloc(igraphmodule_VertexSeqObject* self) { } Py_CLEAR(self->gref); - PY_FREE_AND_DECREF_TYPE(self); + PY_FREE_AND_DECREF_TYPE(self, igraphmodule_VertexSeqType); } /** From fd549a6ea9e4e9680f269a66b99b6ed520846478 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 16:37:44 +0200 Subject: [PATCH 0415/1681] fix: tp_traverse() needs to visit Py_TYPE(self) only in Python >= 3.9 --- src/_igraph/bfsiter.c | 5 ++++- src/_igraph/dfsiter.c | 5 ++++- src/_igraph/graphobject.c | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/_igraph/bfsiter.c b/src/_igraph/bfsiter.c index 03d5308e3..f4b384bdf 100644 --- a/src/_igraph/bfsiter.c +++ b/src/_igraph/bfsiter.c @@ -121,7 +121,10 @@ static int igraphmodule_BFSIter_traverse(igraphmodule_BFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("BFSIter", self); Py_VISIT(self->gref); - Py_VISIT(Py_TYPE(self)); /* needed because heap-allocated types are refcounted */ +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) + Py_VISIT(Py_TYPE(self)); +#endif return 0; } diff --git a/src/_igraph/dfsiter.c b/src/_igraph/dfsiter.c index 12fb9a503..b3dd00cc9 100644 --- a/src/_igraph/dfsiter.c +++ b/src/_igraph/dfsiter.c @@ -122,7 +122,10 @@ static int igraphmodule_DFSIter_traverse(igraphmodule_DFSIterObject *self, visitproc visit, void *arg) { RC_TRAVERSE("DFSIter", self); Py_VISIT(self->gref); - Py_VISIT(Py_TYPE(self)); /* needed because heap-allocated types are refcounted */ +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) + Py_VISIT(Py_TYPE(self)); +#endif return 0; } diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index af80fa817..429e183cd 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -93,7 +93,10 @@ int igraphmodule_Graph_traverse(igraphmodule_GraphObject * self, } } +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) Py_VISIT(Py_TYPE(self)); +#endif return 0; } From 81048cd75fd220e12ab6da80356c2c27221cad6d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 17:24:18 +0200 Subject: [PATCH 0416/1681] test: refcount-related tests should not be run on PyPy --- tests/test_basic.py | 5 ++++- tests/test_cliques.py | 5 +---- tests/test_edgeseq.py | 5 +---- tests/test_foreign.py | 5 +---- tests/test_vertexseq.py | 5 +---- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index db7146d30..aed0677b5 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -22,6 +22,8 @@ ) from igraph._igraph import EdgeSeq as _EdgeSeq, VertexSeq as _VertexSeq +from .utils import is_pypy + try: import numpy as np except ImportError: @@ -826,7 +828,8 @@ def suite(): graph_tuple_list_suite = unittest.makeSuite(GraphTupleListTests) degree_sequence_suite = unittest.makeSuite(DegreeSequenceTests) inheritance_suite = unittest.makeSuite(InheritanceTests) - refcount_suite = unittest.makeSuite(ReferenceCountTests) + if not is_pypy: + refcount_suite = unittest.makeSuite(ReferenceCountTests) return unittest.TestSuite( [ basic_suite, diff --git a/tests/test_cliques.py b/tests/test_cliques.py index 064363e5a..62017c17e 100644 --- a/tests/test_cliques.py +++ b/tests/test_cliques.py @@ -2,10 +2,7 @@ from igraph import * -try: - from .utils import temporary_file -except ImportError: - from utils import temporary_file +from .utils import temporary_file class CliqueTests(unittest.TestCase): diff --git a/tests/test_edgeseq.py b/tests/test_edgeseq.py index f1b451512..74667b758 100644 --- a/tests/test_edgeseq.py +++ b/tests/test_edgeseq.py @@ -4,10 +4,7 @@ from igraph import * -try: - from .utils import is_pypy -except ImportError: - from utils import is_pypy +from .utils import is_pypy try: import numpy as np diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 5d5243e4b..e86051b27 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -4,10 +4,7 @@ from igraph import Graph, InternalError -try: - from .utils import temporary_file -except ImportError: - from utils import temporary_file +from .utils import temporary_file try: import networkx as nx diff --git a/tests/test_vertexseq.py b/tests/test_vertexseq.py index 89c9e8896..12adf1aa1 100644 --- a/tests/test_vertexseq.py +++ b/tests/test_vertexseq.py @@ -4,10 +4,7 @@ from igraph import * -try: - from .utils import is_pypy -except ImportError: - from utils import is_pypy +from .utils import is_pypy try: import numpy as np From 53b540e430e5d4f97e460e0c4110d458f9f4c574 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Sep 2021 17:45:33 +0200 Subject: [PATCH 0417/1681] test: pytest test discovery still discovered refcount tests on PyPy, this is now fixed --- tests/test_basic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_basic.py b/tests/test_basic.py index aed0677b5..340621966 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -781,6 +781,7 @@ def assert_reference_not_leaked(case, *args): case.assertListEqual(refs_before, refs_after) +@unittest.skipIf(is_pypy, "reference counts are not relevant for PyPy") class ReferenceCountTests(unittest.TestCase): def testEdgeReferenceCounting(self): with assert_reference_not_leaked(self, Edge, EdgeSeq, _EdgeSeq): @@ -828,8 +829,7 @@ def suite(): graph_tuple_list_suite = unittest.makeSuite(GraphTupleListTests) degree_sequence_suite = unittest.makeSuite(DegreeSequenceTests) inheritance_suite = unittest.makeSuite(InheritanceTests) - if not is_pypy: - refcount_suite = unittest.makeSuite(ReferenceCountTests) + refcount_suite = unittest.makeSuite(ReferenceCountTests) return unittest.TestSuite( [ basic_suite, From 7b14aaaedebb3c3071a268e50c835cc6919d4dc7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 15 Sep 2021 00:14:25 +1000 Subject: [PATCH 0418/1681] Revamping (yes!) the plotting interface: matplotlib and plotly (#425) * Initial works on vertex and edge drawers * Introduce more mpl switches across the library * Matrix drawing factories * Support palette plotting in mpl * Switch igraph.plot, works * Mark groups transitioning to general infra * Full switch to infra except labels * Move dendrogram plotting code into module * Move more Cairo stuff out of Dendrogram * Dendrogram plot in matplotlib * Edge labels (no alignment yet) * Rounded hull for clustering * Edge label alignment * Try to restructure the Plot class via composition * Testing infrastructure for matplotlib plots * Store baseline images properly, plotting tests work now in CLI * Despine matplotlib plot * Plotting works * More plotting tests * Fix plotting test skipping within unittest * Tests for mpl post-facto editing of vertices * Doc on new config option for matplotlib backend * Bugfix on vertex labels and improved tutorial * doc: the Configuration singleton can be accessed as ig.config * fix: make sure that the backend variable is one of the known backends * chore: format source code using black * fix: remove _checked_on_freetype_version decorator to get rid of dependency on pytest * chore: reformatted tests/drawing with black * test: use a fixed circle layout for the large clustering test; the default layout depends on the random seed a bit * refactor: renamed DefaultDendrogramDrawer to CairoDendrogramDrawer * refactor: keep DefaultDendrogramDrawer as an alias * doc: remove all references to Matplotlib from the documentation of CairoPlot * fix: fix plotting of Palette objects * chore: code formatting, remove unused import * fix: fix plotting with the Cairo backend so the "margin" argument takes effect properly * Support matplotlib plot for histogram * Remove matplotlib example/demo folder, will do a separate PR * remove bbox and palette args and deprecation warn for *args in draw * Small bugfix and typo * chore: nitpicking * fix: fix drawing of palettes in Cairo * refactor: started separating Cairo-related stuff into igraph.drawing.cairo [WIP] * refactor: remove default image viewer config setting and support for invoking an external image viewer automatically * fix: use ABCMeta and @abstractmethod annotations for abstract methods * refactor: moved TextDrawer and AbstractCairoDrawer to Cairo-specific module * refactor: DescartesCoordinateSystem is now in igraph.drawing.cairo * refactor: vertex drawers moved to Cairo or Matplotlib-specific submodules * refactor: edge drawers are now also in the correct (Cairo or Matplotlib) submodule * refactor: matrix drawers are now in the Matplotlib and Cairo submodules * chore: removed deprecated UbiGraphDrawer, updated changelog * refactor: graph drawers moved to Cairo or Matplotlib submodule * refactor: dendrogram drawers are now in the Matplotlib and Cairo submodules * refactor: polygon drawers are now in the Cairo and Matplotlib packages, respectively * Drawer factory * Forgot one file * Start supporting plotly * Empty __init__ file for plotly * More plotly code, perhaps could work * Typo in edge * more typos * Passes mpl tests locally * Passes basic tests for plotly * Passes all plotly tests (no control image) * Testing suite for plotly (first version) * Cairo unit tests * Cairo passes all tests * fix: make sure that result_images/cairo exists; also handle Path objects properly with cairocffi * fix: make sure that Cairo, Matplotlib and Plotly are all installed in the test env and fix find_open_image_png_function() for PIL * fix: use an easily diffable JSON representation for expected Plotly images * fix: move Matplotlib baseline images to their right places * fix: make sure that plot dependencies are installed when building wheels * fix: temporarily remove Cairo from CI tests; it requires additional deps in the CI env * fix: apparently Matplotlib tests also require pytest * Switch mpl tests to PDF and move other images around * variable not needed * Use png images * fix * Skip entire test module if missing mpl/plotly/cairo * Fix layout for plotly and investigate graph_objects import * Increase tolerance on mpl, fix layout on plotly * Plotly figure dict comparator with tolerance * Back to PDF (should work on i686) and install ghostscript on CI * Ugly custom converter lifted from mpl to enforce ghostscript as gs * Try encorcing manylinux2014 which should have ghostscript 9 * Fix layout in mpl tests * Revert to PNG, ghostscript conversion is unreliable across archs and old Python versions * Mysterious failure on numpy for pypy and OSX, trying switching to pytest * Try skipping mpl/numpy on pypy * chore: updated cibuildwheel to 2.1.1 * ci: skip testing on Python 3.10 until NumPy / SciPy / Pandas publish wheels * test: allow using older Matplotlib version because of Python 3.6 * test: don't install cairocffi in CI env because we don't have Cairo DLLs there * test: use pytest with PyPy as well * fix: don't require matplotlib >= 3.4 because that would exclude Python 3.6 users from using matplotlib [ci skip] Co-authored-by: Tamas Nepusz --- .github/workflows/build.yml | 13 +- .gitignore | 1 + CHANGELOG.md | 89 +- .../figures/tutorial_social_network_1_mpl.png | Bin 0 -> 24681 bytes .../figures/tutorial_social_network_2_mpl.png | Bin 0 -> 30944 bytes doc/source/tutorial.rst | 23 +- doc/source/visualisation.rst | 20 +- setup.py | 10 +- src/igraph/__init__.py | 109 +- src/igraph/app/shell.py | 6 +- src/igraph/clustering.py | 232 +- src/igraph/configuration.py | 93 +- src/igraph/datatypes.py | 163 +- src/igraph/drawing/__init__.py | 589 +--- src/igraph/drawing/baseclasses.py | 335 +- src/igraph/drawing/cairo/__init__.py | 3 + src/igraph/drawing/cairo/base.py | 84 + src/igraph/drawing/{ => cairo}/coord.py | 22 +- src/igraph/drawing/cairo/dendrogram.py | 247 ++ src/igraph/drawing/{ => cairo}/edge.py | 190 +- src/igraph/drawing/cairo/graph.py | 425 +++ src/igraph/drawing/cairo/histogram.py | 59 + src/igraph/drawing/cairo/matrix.py | 250 ++ src/igraph/drawing/cairo/palette.py | 53 + src/igraph/drawing/cairo/plot.py | 360 ++ src/igraph/drawing/cairo/polygon.py | 86 + src/igraph/drawing/cairo/text.py | 358 ++ src/igraph/drawing/cairo/utils.py | 20 + src/igraph/drawing/{ => cairo}/vertex.py | 44 +- src/igraph/drawing/colors.py | 64 +- src/igraph/drawing/graph.py | 1032 +----- src/igraph/drawing/matplotlib/__init__.py | 0 src/igraph/drawing/matplotlib/dendrogram.py | 176 + src/igraph/drawing/matplotlib/edge.py | 274 ++ src/igraph/drawing/matplotlib/graph.py | 317 ++ src/igraph/drawing/matplotlib/histogram.py | 37 + src/igraph/drawing/matplotlib/matrix.py | 26 + src/igraph/drawing/matplotlib/palette.py | 49 + src/igraph/drawing/matplotlib/polygon.py | 88 + src/igraph/drawing/matplotlib/utils.py | 26 + src/igraph/drawing/matplotlib/vertex.py | 71 + src/igraph/drawing/plotly/__init__.py | 0 src/igraph/drawing/plotly/edge.py | 270 ++ src/igraph/drawing/plotly/graph.py | 281 ++ src/igraph/drawing/plotly/polygon.py | 104 + src/igraph/drawing/plotly/utils.py | 74 + src/igraph/drawing/plotly/vertex.py | 95 + src/igraph/drawing/shapes.py | 194 +- src/igraph/drawing/text.py | 367 +- src/igraph/drawing/utils.py | 250 +- src/igraph/formula.py | 3 +- src/igraph/statistics.py | 35 +- src/igraph/utils.py | 48 - tests/drawing/__init__.py | 0 tests/drawing/cairo/__init__.py | 0 .../baseline_images/clustering_directed.png | Bin 0 -> 30616 bytes .../clustering_directed_large.png | Bin 0 -> 64832 bytes .../cairo/baseline_images/graph_basic.png | Bin 0 -> 17231 bytes .../cairo/baseline_images/graph_directed.png | Bin 0 -> 17911 bytes .../graph_mark_groups_directed.png | Bin 0 -> 17911 bytes .../graph_mark_groups_squares_directed.png | Bin 0 -> 15413 bytes tests/drawing/cairo/test_graph.py | 99 + tests/drawing/cairo/utils.py | 170 + tests/drawing/matplotlib/__init__.py | 0 .../test_graph/clustering_directed.png | Bin 0 -> 36485 bytes .../test_graph/clustering_directed_large.png | Bin 0 -> 67108 bytes .../test_graph/graph_basic.png | Bin 0 -> 25084 bytes .../test_graph/graph_directed.png | Bin 0 -> 26545 bytes .../test_graph/graph_edit_children.png | Bin 0 -> 24552 bytes .../test_graph/graph_mark_groups_directed.png | Bin 0 -> 26545 bytes tests/drawing/matplotlib/test_graph.py | 176 + tests/drawing/matplotlib/utils.py | 122 + tests/drawing/plotly/__init__.py | 0 .../baseline_images/clustering_directed.json | 1056 ++++++ .../clustering_directed_large.json | 2991 +++++++++++++++++ .../plotly/baseline_images/graph_basic.json | 963 ++++++ .../baseline_images/graph_directed.json | 1048 ++++++ .../baseline_images/graph_edit_children.json | 963 ++++++ .../graph_mark_groups_directed.json | 1048 ++++++ tests/drawing/plotly/test_graph.py | 185 + tests/drawing/plotly/utils.py | 274 ++ tests/test_attributes.py | 3 +- tox.ini | 5 +- 83 files changed, 13925 insertions(+), 2943 deletions(-) create mode 100644 doc/source/figures/tutorial_social_network_1_mpl.png create mode 100644 doc/source/figures/tutorial_social_network_2_mpl.png create mode 100644 src/igraph/drawing/cairo/__init__.py create mode 100644 src/igraph/drawing/cairo/base.py rename src/igraph/drawing/{ => cairo}/coord.py (85%) create mode 100644 src/igraph/drawing/cairo/dendrogram.py rename src/igraph/drawing/{ => cairo}/edge.py (64%) create mode 100644 src/igraph/drawing/cairo/graph.py create mode 100644 src/igraph/drawing/cairo/histogram.py create mode 100644 src/igraph/drawing/cairo/matrix.py create mode 100644 src/igraph/drawing/cairo/palette.py create mode 100644 src/igraph/drawing/cairo/plot.py create mode 100644 src/igraph/drawing/cairo/polygon.py create mode 100644 src/igraph/drawing/cairo/text.py create mode 100644 src/igraph/drawing/cairo/utils.py rename src/igraph/drawing/{ => cairo}/vertex.py (63%) create mode 100644 src/igraph/drawing/matplotlib/__init__.py create mode 100644 src/igraph/drawing/matplotlib/dendrogram.py create mode 100644 src/igraph/drawing/matplotlib/edge.py create mode 100644 src/igraph/drawing/matplotlib/graph.py create mode 100644 src/igraph/drawing/matplotlib/histogram.py create mode 100644 src/igraph/drawing/matplotlib/matrix.py create mode 100644 src/igraph/drawing/matplotlib/palette.py create mode 100644 src/igraph/drawing/matplotlib/polygon.py create mode 100644 src/igraph/drawing/matplotlib/utils.py create mode 100644 src/igraph/drawing/matplotlib/vertex.py create mode 100644 src/igraph/drawing/plotly/__init__.py create mode 100644 src/igraph/drawing/plotly/edge.py create mode 100644 src/igraph/drawing/plotly/graph.py create mode 100644 src/igraph/drawing/plotly/polygon.py create mode 100644 src/igraph/drawing/plotly/utils.py create mode 100644 src/igraph/drawing/plotly/vertex.py create mode 100644 tests/drawing/__init__.py create mode 100644 tests/drawing/cairo/__init__.py create mode 100644 tests/drawing/cairo/baseline_images/clustering_directed.png create mode 100644 tests/drawing/cairo/baseline_images/clustering_directed_large.png create mode 100644 tests/drawing/cairo/baseline_images/graph_basic.png create mode 100644 tests/drawing/cairo/baseline_images/graph_directed.png create mode 100644 tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png create mode 100644 tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png create mode 100644 tests/drawing/cairo/test_graph.py create mode 100644 tests/drawing/cairo/utils.py create mode 100644 tests/drawing/matplotlib/__init__.py create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed.png create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed_large.png create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/graph_basic.png create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/graph_directed.png create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/graph_edit_children.png create mode 100644 tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_directed.png create mode 100644 tests/drawing/matplotlib/test_graph.py create mode 100644 tests/drawing/matplotlib/utils.py create mode 100644 tests/drawing/plotly/__init__.py create mode 100644 tests/drawing/plotly/baseline_images/clustering_directed.json create mode 100644 tests/drawing/plotly/baseline_images/clustering_directed_large.json create mode 100644 tests/drawing/plotly/baseline_images/graph_basic.json create mode 100644 tests/drawing/plotly/baseline_images/graph_directed.json create mode 100644 tests/drawing/plotly/baseline_images/graph_edit_children.json create mode 100644 tests/drawing/plotly/baseline_images/graph_mark_groups_directed.json create mode 100644 tests/drawing/plotly/test_graph.py create mode 100644 tests/drawing/plotly/utils.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed176ec2b..afd7a2df1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,8 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - CIBW_SKIP: "cp27-* pp27-* cp35-*" + # skip testing on Python 3.10 until NumPy/SciPy/Pandas publish wheels + CIBW_TEST_SKIP: "cp310-*" jobs: build_wheels: @@ -26,7 +27,7 @@ jobs: python-version: '3.8' - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" @@ -50,7 +51,7 @@ jobs: uses: docker/setup-qemu-action@v1 - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 @@ -88,7 +89,7 @@ jobs: brew install ninja autoconf automake libtool cmake - name: Build wheels - uses: joerick/cibuildwheel@v1.10.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_ARCHS_MACOS: "x86_64 arm64" CIBW_BEFORE_BUILD: "python setup.py build_c_core" @@ -145,16 +146,16 @@ jobs: shell: cmd - name: Build wheels - uses: joerick/cibuildwheel@v1.11.0 + uses: joerick/cibuildwheel@v2.1.1 env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" + CIBW_TEST_COMMAND: "cd /d {project} && python -m pytest tests" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ IGRAPH_STATIC_EXTENSION: True IGRAPH_EXTRA_LIBRARIES: libxml2,lzma,zlib,iconv,charset IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 - CIBW_TEST_COMMAND: "cd /d {project} && python -m pytest tests" - uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index 2391ed900..44657c996 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/ dist/ doc/api/ src/igraph/*.so +result_images/ *.egg-info/ .python-version .eggs/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d9c4755..bc13f2d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,22 @@ ### Changed -* Improved performance of `Graph.DataFrame()`, thanks to +- Improved performance of `Graph.DataFrame()`, thanks to [@fwitter](https://github.com/user/fwitter). See PR [#418](https://github.com/igraph/python-igraph/pull/418) for more details. +### Removed + +- Removed deprecated `UbiGraphDrawer`. + +- Removed deprecated `show()` method of `Plot` instances as well as the feature + that automatically shows the plot when `plot()` is called with no target. + ## [0.9.6] ### Fixed -* Version 0.9.5 accidentally broke the Matplotlib backend when it was invoked +- Version 0.9.5 accidentally broke the Matplotlib backend when it was invoked without the `mark_groups=...` keyword argument; this version fixes the issue. Thanks to @dschult for reporting it! @@ -20,13 +27,13 @@ ### Fixed -* `plot(g, ..., mark_groups=True)` now works with the Matplotlib plotting backend. +- `plot(g, ..., mark_groups=True)` now works with the Matplotlib plotting backend. -* `set_random_number_generator(None)` now correctly switches back to igraph's +- `set_random_number_generator(None)` now correctly switches back to igraph's own random number generator instead of the default one that hooks into the `random` module of Python. -* Improved performance in cases when igraph has to call back to Python's +- Improved performance in cases when igraph has to call back to Python's `random` module to generate random numbers. One example is `Graph.Degree_Sequence(method="vl")`, whose performance suffered a more than 30x slowdown on 32-bit platforms before, compared to the native C @@ -39,101 +46,100 @@ ### Added -* Added `Graph.is_tree()` to test whether a graph is a tree. +- Added `Graph.is_tree()` to test whether a graph is a tree. -* Added `Graph.Realize_Degree_Sequence()` to construct a graph that realizes a +- Added `Graph.Realize_Degree_Sequence()` to construct a graph that realizes a given degree sequence, using a deterministic (Havel-Hakimi-style) algorithm. -* Added `Graph.Tree_Game()` to generate random trees with uniform sampling. +- Added `Graph.Tree_Game()` to generate random trees with uniform sampling. -* `Graph.to_directed()` now supports a `mode=...` keyword argument. +- `Graph.to_directed()` now supports a `mode=...` keyword argument. -* Added a `create_using=...` keyword argument to `Graph.to_networkx()` to +- Added a `create_using=...` keyword argument to `Graph.to_networkx()` to let the user specify which NetworkX class to use when converting the graph. ### Changed -* Updated igraph dependency to 0.9.4. +- Updated igraph dependency to 0.9.4. ### Fixed -* Improved performance of `Graph.from_networkx()` and `Graph.from_graph_tool()` +- Improved performance of `Graph.from_networkx()` and `Graph.from_graph_tool()` on large graphs, thanks to @szhorvat and @iosonofabio for fixing the issue. -* Fixed the `autocurve=...` keyword argument of `plot()` when using the +- Fixed the `autocurve=...` keyword argument of `plot()` when using the Matplotlib backend. ### Deprecated -* Functions and methods that take string arguments that represent an underlying +- Functions and methods that take string arguments that represent an underlying enum in the C core of igraph now print a deprecation warning when provided with a string that does not match one of the enum member names (as documented in the docstrings) exactly. Partial matches will be removed in the next minor or major version, whichever comes first. -* `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. +- `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. -* `igraph.graph.drawing.UbiGraphDrawer` is deprecated as the upstream project +- `igraph.graph.drawing.UbiGraphDrawer` is deprecated as the upstream project is not maintained since 2008. ## [0.9.1] ### Changed -* Calling `plot()` without a filename or a target surface is now deprecated. +- Calling `plot()` without a filename or a target surface is now deprecated. The original intention was to plot to a temporary file and then open it in the default image viewer of the platform of the user automatically, but this has never worked reliably. The feature will be removed in 0.10.0. ### Fixed -* Fixed plotting of `VertexClustering` objects on Matplotlib axes. +- Fixed plotting of `VertexClustering` objects on Matplotlib axes. -* The `IGRAPH_CMAKE_EXTRA_ARGS` environment variable is now applied _after_ the +- The `IGRAPH_CMAKE_EXTRA_ARGS` environment variable is now applied _after_ the default CMake arguments when building the C core of igraph from source. This enables package maintainers to override any of the default arguments we pass to CMake. -* Fixed the documentation build by replacing Epydoc with PyDoctor. +- Fixed the documentation build by replacing Epydoc with PyDoctor. ### Miscellaneous -* Building `python-igraph` from source should not require `flex` and `bison` +- Building `python-igraph` from source should not require `flex` and `bison` any more; sources of the parsers used by the C core are now included in the Python source tarball. -* Many old code constructs that were used to maintain compatibility with Python +- Many old code constructs that were used to maintain compatibility with Python 2.x are removed now that we have dropped support for Python 2.x. -* Reading GraphML files is now also supported on Windows if you use one of the +- Reading GraphML files is now also supported on Windows if you use one of the official Python wheels. - ## [0.9.0] ### Added -* `Graph.DataFrame` now has a `use_vids=...` keyword argument that decides whether +- `Graph.DataFrame` now has a `use_vids=...` keyword argument that decides whether the data frame contains vertex IDs (`True`) or vertex names (`False`). (PR #348) -* Added `MatplotlibGraphDrawer` to draw a graph on an existing Matplotlib +- Added `MatplotlibGraphDrawer` to draw a graph on an existing Matplotlib figure. (PR #341) -* Added a code path to choose between preferred image viewers on FreeBSD. (PR #354) +- Added a code path to choose between preferred image viewers on FreeBSD. (PR #354) -* Added `Graph.harmonic_centrality()` that wraps `igraph_harmonic_centrality()` +- Added `Graph.harmonic_centrality()` that wraps `igraph_harmonic_centrality()` from the underlying C library. ### Changed -* `python-igraph` is now compatible with `igraph` 0.9.0. +- `python-igraph` is now compatible with `igraph` 0.9.0. -* The setup script was adapted to the new CMake-based build system of `igraph`. +- The setup script was adapted to the new CMake-based build system of `igraph`. -* Dropped support for older Python versions; the oldest Python version that +- Dropped support for older Python versions; the oldest Python version that `python-igraph` is tested on is now Python 3.6. -* The default splitting heuristic of the BLISS isomorphism algorithm was changed +- The default splitting heuristic of the BLISS isomorphism algorithm was changed from `IGRAPH_BLISS_FM` (first maximally non-trivially connected non-singleton cell) to `IGRAPH_BLISS_FL` (first largest non-singleton cell) as this seems to provide better performance on a variety of graph classes. This change is a follow-up @@ -141,31 +147,30 @@ ### Fixed -* Fixed crashes in the Python-C glue code related to the handling of empty +- Fixed crashes in the Python-C glue code related to the handling of empty vectors in certain attribute merging functions (see issue #358). -* Fixed a memory leak in `Graph.closeness_centrality()` when an invalid `cutoff` +- Fixed a memory leak in `Graph.closeness_centrality()` when an invalid `cutoff` argument was provided to the function. -* Clarified that the `fixed=...` argument is ineffective for the DrL layout +- Clarified that the `fixed=...` argument is ineffective for the DrL layout because the underlying C code does not handle it. The argument was _not_ removed for sake of backwards compatibility. -* `VertexSeq.find(name=x)` now works correctly when `x` is an integer; fixes +- `VertexSeq.find(name=x)` now works correctly when `x` is an integer; fixes #367 ### Miscellaneous -* The Python codebase was piped through `black` for consistent formatting. +- The Python codebase was piped through `black` for consistent formatting. -* Wildcard imports were removed from the codebase. +- Wildcard imports were removed from the codebase. -* CI tests were moved to Github Actions from Travis. +- CI tests were moved to Github Actions from Travis. -* The core C library is now built with `-fPIC` on Linux to allow linking to the +- The core C library is now built with `-fPIC` on Linux to allow linking to the Python interface. - ## [0.8.3] This is the last released version of `python-igraph` without a changelog file. @@ -173,7 +178,7 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[Unreleased]: https://github.com/igraph/python-igraph/compare/0.9.6..HEAD +[unreleased]: https://github.com/igraph/python-igraph/compare/0.9.6..HEAD [0.9.6]: https://github.com/igraph/python-igraph/compare/0.9.5...0.9.6 [0.9.5]: https://github.com/igraph/python-igraph/compare/0.9.4...0.9.5 [0.9.4]: https://github.com/igraph/python-igraph/compare/0.9.1...0.9.4 diff --git a/doc/source/figures/tutorial_social_network_1_mpl.png b/doc/source/figures/tutorial_social_network_1_mpl.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b6bf4d323d47f656768d76d00b210ee4293a01 GIT binary patch literal 24681 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 literal 0 HcmV?d00001 diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index b9d7cf25e..8f732c00f 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -659,8 +659,9 @@ the screen or to a PDF, PNG or SVG file using the `Cairo library >> fig, ax = plt.subplots() >>> plot(g, layout=layout, target=ax) +.. figure:: figures/tutorial_social_network_1_mpl.png + :alt: The visual representation of our social network (matplotlib backend) + :align: center + Hmm, this is not too pretty so far. A trivial addition would be to use the names as the vertex labels and to color the vertices according to the gender. Vertex labels are taken from the ``label`` attribute by default and vertex colors are determined by the @@ -765,8 +770,8 @@ from the ``label`` attribute by default and vertex colors are determined by the >>> g.vs["label"] = g.vs["name"] >>> color_dict = {"m": "blue", "f": "pink"} >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] ->>> plot(g, layout=layout, bbox=(300, 300), margin=20) ->>> plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib version +>>> plot(g, layout=layout, bbox=(300, 300), margin=20) # Cairo backend +>>> plot(g, layout=layout, target=ax) # matplotlib backend Note that we are simply re-using the previous layout object here, but we also specified that we need a smaller plot (300 x 300 pixels) and a larger margin around the graph @@ -778,6 +783,14 @@ to fit the labels (20 pixels). The result is: Our social network - with names as labels and genders as colors +and for matplotlib: + +.. figure:: figures/tutorial_social_network_2_mpl.png + :alt: The visual representation of our social network - with names and genders + :align: center + + Our social network - with names as labels and genders as colors + Instead of specifying the visual properties as vertex and edge attributes, you can also give them as keyword arguments to :func:`plot`: diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index a67fc90be..629324cd4 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -137,13 +137,27 @@ you might want to change the size and color of the vertices: >>> import matplotlib.pyplot as plt >>> fig, ax = plt.subplots() >>> ig.plot(g, target=ax) ->>> dots = ax.get_children()[0] # This is a PathCollection ->>> dots.set_color('tomato') ->>> dots.set_sizes([250] * g.vcount()) +>>> dot = ax.get_children()[0] # This is a Circle for the first vertex +>>> dot.set_color('tomato') +>>> dot.radius *= 2 # double the default radius That also helps as a workaround if you cannot figure out how to use the plotting options below: just use the defaults and then customize the appearance of your graph via standard `matplotlib`_ tools. +.. note:: The order of `ax.get_children()` is the following: (i) patches for clustering hulls if requested; + (ii) patches for vertices; (iii) patches for edges: for undirected graphs, there's one patch per edge. For directed graphs, there's a *pair* of patches, associated with the arrow body and head, respectively. + +To use `matplotlib_` as your default plotting backend, you can set: + +>>> ig.config['plotting.backend'] = 'matplotlib' + +Then you don't have to specify an `Axes` anymore: + +>>> ig.plot(g) + +will automatically make a new `Axes` for you and return it. + + Plotting graphs in Jupyter notebooks ++++++++++++++++++++++++++++++++++++ |igraph| supports inline plots within a `Jupyter`_ notebook via both the `Cairo`_ and `matplotlib`_ backend. If you are diff --git a/setup.py b/setup.py index 5ba9f0e12..e9a1c647b 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ ########################################################################### -from setuptools import setup, Command, Extension +from setuptools import find_packages, setup, Command, Extension import glob import shlex @@ -819,10 +819,14 @@ def use_educated_guess(self) -> None: author_email="ntamas@gmail.com", ext_modules=[igraph_extension], package_dir={"igraph": "src/igraph"}, - packages=["igraph", "igraph.app", "igraph.drawing", "igraph.remote"], + packages=find_packages(where="src"), scripts=["scripts/igraph"], install_requires=["texttable>=1.6.2"], extras_require={ + "cairo": ["cairocffi>=1.2.0"], + "matplotlib": ["matplotlib>=3.3.0; platform_python_implementation != 'PyPy'"], + "plotly": ["plotly>=5.3.0"], + # compatibility alias to 'cairo' for python-igraph <= 0.9.6 "plotting": ["cairocffi>=1.2.0"], "test": [ "networkx>=2.5", @@ -830,6 +834,8 @@ def use_educated_guess(self) -> None: "numpy>=1.19.0; platform_python_implementation != 'PyPy'", "pandas>=1.1.0,<1.3.1; platform_python_implementation != 'PyPy'", "scipy>=1.5.0; platform_python_implementation != 'PyPy'", + "matplotlib>=3.3.4; platform_python_implementation != 'PyPy'", + "plotly>=5.3.0", ], }, python_requires=">=3.6", diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 530cbc3cb..31609a5ce 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -74,7 +74,7 @@ set_progress_handler, set_random_number_generator, set_status_handler, - __igraph_version__ + __igraph_version__, ) from igraph.clustering import ( Clustering, @@ -89,7 +89,16 @@ ) from igraph.cut import Cut, Flow from igraph.configuration import Configuration, init as init_configuration -from igraph.drawing import BoundingBox, DefaultGraphDrawer, Plot, Point, Rectangle, plot +from igraph.drawing import ( + BoundingBox, + CairoGraphDrawer, + DefaultGraphDrawer, + MatplotlibGraphDrawer, + Plot, + Point, + Rectangle, + plot, +) from igraph.drawing.colors import ( Palette, GradientPalette, @@ -732,9 +741,7 @@ def get_adjacency_sparse(self, attribute=None): try: from scipy import sparse except ImportError: - raise ImportError( - "You should install scipy in order to use this function" - ) + raise ImportError("You should install scipy in order to use this function") edges = self.get_edgelist() if attribute is None: @@ -2454,7 +2461,7 @@ def write_svg( edge_stroke_widths="width", font_size=16, *args, - **kwds + **kwds, ): """Saves the graph as an SVG (Scalable Vector Graphics) file @@ -2622,13 +2629,13 @@ def write_svg( print(' id="pathArrow{0}"'.format(marker_index), file=f) print( ' style="font-size:12.0;fill-rule:evenodd;' - 'stroke-width:0.62500000;stroke-linejoin:round;' + "stroke-width:0.62500000;stroke-linejoin:round;" 'fill:{0}"'.format(e_col), file=f, ) print( ' d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 ' - 'L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 ' + "L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 " '6.9831476,1.6157441 8.7185878,4.0337352 z "', file=f, ) @@ -2656,7 +2663,7 @@ def write_svg( print("'.format( + "/>".format( vertex_width / 2.0, vertex_height / 2.0, vertex_width, @@ -2779,19 +2786,15 @@ def write_svg( print( ' '.format( - vertex_size / 2.0, vidx, font_size - ), + "font-weight:normal;text-align:center;line-height:125%;" + "letter-spacing:0px;word-spacing:0px;text-anchor:middle;" + "fill:#000000;fill-opacity:1;stroke:none;" + 'font-family:Sans">'.format(vertex_size / 2.0, vidx, font_size), file=f, ) print( '' - '{2}'.format( - vertex_size / 2.0, vidx, str(labels[vidx]) - ), + "{2}".format(vertex_size / 2.0, vidx, str(labels[vidx])), file=f, ) print(" ", file=f) @@ -3345,7 +3348,7 @@ def Incidence( multiple=False, weighted=None, *args, - **kwds + **kwds, ): """Creates a bipartite graph from an incidence matrix. @@ -3440,26 +3443,41 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): if edges.shape[1] < 2: raise ValueError("The 'edges' DataFrame must contain at least two columns") if vertices is not None and vertices.shape[1] < 1: - raise ValueError("The 'vertices' DataFrame must contain at least one column") + raise ValueError( + "The 'vertices' DataFrame must contain at least one column" + ) if use_vids: - if not (str(edges.dtypes[0]).startswith("int") and str(edges.dtypes[1]).startswith("int")): - raise TypeError(f"Source and target IDs must be 0-based integers, found types {edges.dtypes.tolist()[:2]}") + if not ( + str(edges.dtypes[0]).startswith("int") + and str(edges.dtypes[1]).startswith("int") + ): + raise TypeError( + f"Source and target IDs must be 0-based integers, found types {edges.dtypes.tolist()[:2]}" + ) elif (edges.iloc[:, :2] < 0).any(axis=None): raise ValueError("Source and target IDs must not be negative") if vertices is not None: vertices = vertices.sort_index() - if not vertices.index.equals(pd.RangeIndex.from_range(range(vertices.shape[0]))): + if not vertices.index.equals( + pd.RangeIndex.from_range(range(vertices.shape[0])) + ): if not str(vertices.index.dtype).startswith("int"): - raise TypeError(f"Vertex IDs must be 0-based integers, found type {vertices.index.dtype}") + raise TypeError( + f"Vertex IDs must be 0-based integers, found type {vertices.index.dtype}" + ) elif (vertices.index < 0).any(axis=None): raise ValueError("Vertex IDs must not be negative") else: - raise ValueError(f"Vertex IDs must be an integer sequence from 0 to {vertices.shape[0] - 1}") + raise ValueError( + f"Vertex IDs must be an integer sequence from 0 to {vertices.shape[0] - 1}" + ) else: # Handle if some source and target names in 'edges' are 'NA' if edges.iloc[:, :2].isna().any(axis=None): - warn("In the first two columns of 'edges' NA elements were replaced with string \"NA\"") + warn( + "In the first two columns of 'edges' NA elements were replaced with string \"NA\"" + ) edges = edges.copy() edges.iloc[:, :2].fillna("NA", inplace=True) @@ -3468,7 +3486,9 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): vertices = pd.DataFrame({"name": np.unique(edges.values[:, :2])}) if vertices.iloc[:, 0].isna().any(): - warn("In the first column of 'vertices' NA elements were replaced with string \"NA\"") + warn( + "In the first column of 'vertices' NA elements were replaced with string \"NA\"" + ) vertices = vertices.copy() vertices.iloc[:, 0].fillna("NA", inplace=True) @@ -3476,9 +3496,13 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): raise ValueError("Vertex names must be unique") if vertices.shape[1] > 1 and "name" in vertices.columns[1:]: - raise ValueError("Vertex attribute conflict: DataFrame already contains column 'name'") + raise ValueError( + "Vertex attribute conflict: DataFrame already contains column 'name'" + ) - vertices = vertices.rename({vertices.columns[0]: "name"}, axis=1).reset_index(drop=True) + vertices = vertices.rename( + {vertices.columns[0]: "name"}, axis=1 + ).reset_index(drop=True) # Map source and target names in 'edges' to IDs vid_map = pd.Series(vertices.index, index=vertices.iloc[:, 0]) @@ -3492,7 +3516,9 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): g = Graph(n=nv, directed=directed) else: if not edges.iloc[:, :2].isin(vertices.index).all(axis=None): - raise ValueError("Some vertices in the edge DataFrame are missing from vertices DataFrame") + raise ValueError( + "Some vertices in the edge DataFrame are missing from vertices DataFrame" + ) nv = vertices.shape[0] g = Graph(n=nv, directed=directed) # Add vertex attributes @@ -3501,7 +3527,9 @@ def DataFrame(cls, edges, directed=True, vertices=None, use_vids=False): # add edges including optional attributes e_list = list(edges.iloc[:, :2].itertuples(index=False, name=None)) - e_attr = edges.iloc[:, 2:].to_dict(orient='list') if edges.shape[1] > 2 else None + e_attr = ( + edges.iloc[:, 2:].to_dict(orient="list") if edges.shape[1] > 2 else None + ) g.add_edges(e_list, e_attr) return g @@ -3964,8 +3992,8 @@ def __reduce__(self): __iter__ = None # needed for PyPy __hash__ = None # needed for PyPy - def __plot__(self, context, bbox, palette, *args, **kwds): - """Plots the graph to the given Cairo context in the given bounding box + def __plot__(self, backend, context, *args, **kwds): + """Plots the graph to the given Cairo context or matplotlib Axes. The visual style of vertices and edges can be modified at three places in the following order of precedence (lower indices override @@ -4155,11 +4183,14 @@ def __plot__(self, context, bbox, palette, *args, **kwds): specifies whether the order is reversed (C{True}, C{False}, C{"asc"} and C{"desc"} are accepted values). """ - drawer_factory = kwds.get("drawer_factory", DefaultGraphDrawer) - if "drawer_factory" in kwds: - del kwds["drawer_factory"] - drawer = drawer_factory(context, bbox) - drawer.draw(self, palette, *args, **kwds) + from igraph.drawing import DrawerDirectory + + drawer = kwds.pop( + "drawer_factory", + DrawerDirectory.resolve(self, backend)(context), + + ) + drawer.draw(self, *args, **kwds) def __str__(self): """Returns a string representation of the graph. diff --git a/src/igraph/app/shell.py b/src/igraph/app/shell.py index cb450c32a..86f901075 100644 --- a/src/igraph/app/shell.py +++ b/src/igraph/app/shell.py @@ -20,6 +20,7 @@ """ +from abc import ABCMeta, abstractmethod import re import sys @@ -285,14 +286,15 @@ def clear(self): self.last_message = "" -class Shell: +class Shell(metaclass=ABCMeta): """Superclass of the embeddable shells supported by igraph""" def __init__(self): pass + @abstractmethod def __call__(self): - raise NotImplementedError("abstract class") + raise NotImplementedError def supports_progress_bar(self): """Checks whether the shell supports progress bars. diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 67ef19bb7..92631d31c 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -3,16 +3,16 @@ """Classes related to graph clustering.""" from copy import deepcopy -from math import pi from io import StringIO from igraph import community_to_membership from igraph.configuration import Configuration from igraph.datatypes import UniqueIdGenerator from igraph.drawing.colors import ClusterColoringPalette +from igraph.drawing.cairo.dendrogram import CairoDendrogramDrawer +from igraph.drawing.matplotlib.dendrogram import MatplotlibDendrogramDrawer from igraph.statistics import Histogram from igraph.summary import _get_wrapper_for_width -from igraph.utils import str_to_orientation class Clustering: @@ -427,9 +427,8 @@ def giant(self): max_size = max(ss) return self.subgraph(ss.index(max_size)) - def __plot__(self, context, bbox, palette, *args, **kwds): - """Plots the clustering to the given Cairo context in the given - bounding box. + def __plot__(self, backend, context, *args, **kwds): + """Plots the clustering to the given Cairo context or matplotlib Axes. This is done by calling L{Graph.__plot__()} with the same arguments, but coloring the graph vertices according to the current clustering (unless @@ -476,15 +475,18 @@ def __plot__(self, context, bbox, palette, *args, **kwds): @see: L{Graph.__plot__()} for more supported keyword arguments. """ + from igraph.drawing.colors import default_edge_colors + if "edge_color" not in kwds and "color" not in self.graph.edge_attributes(): # Set up a default edge coloring based on internal vs external edges - colors = ["grey20", "grey80"] + colors = default_edge_colors[backend] kwds["edge_color"] = [ colors[is_crossing] for is_crossing in self.crossing() ] + palette = kwds.get('palette', None) if palette is None: - palette = ClusterColoringPalette(len(self)) + kwds['palette'] = ClusterColoringPalette(len(self)) if "mark_groups" not in kwds: if Configuration.instance()["plotting.mark_groups"]: @@ -496,8 +498,9 @@ def __plot__(self, context, bbox, palette, *args, **kwds): if "vertex_color" not in kwds: kwds["vertex_color"] = self.membership + result = self._graph.__plot__(backend, context, *args, **kwds) - return self._graph.__plot__(context, bbox, palette, *args, **kwds) + return result def _formatted_cluster_iterator(self): """Iterates over the clusters and formats them into a string to be @@ -723,46 +726,8 @@ def summary(self, verbosity=0, max_leaf_count=40): return out.getvalue().strip() - def _item_box_size(self, context, horiz, idx): - """Calculates the amount of space needed for drawing an - individual vertex at the bottom of the dendrogram.""" - if self._names is None or self._names[idx] is None: - x_bearing, _, _, height, x_advance, _ = context.text_extents("") - else: - x_bearing, _, _, height, x_advance, _ = context.text_extents( - str(self._names[idx]) - ) - - if horiz: - return x_advance - x_bearing, height - return height, x_advance - x_bearing - - def _plot_item(self, context, horiz, idx, x, y): - """Plots a dendrogram item to the given Cairo context - - @param context: the Cairo context we are plotting on - @param horiz: whether the dendrogram is horizontally oriented - @param idx: the index of the item - @param x: the X position of the item - @param y: the Y position of the item - """ - if self._names is None or self._names[idx] is None: - return - - height = self._item_box_size(context, True, idx)[1] - if horiz: - context.move_to(x, y + height) - context.show_text(str(self._names[idx])) - else: - context.save() - context.translate(x, y) - context.rotate(-pi / 2.0) - context.move_to(0, height) - context.show_text(str(self._names[idx])) - context.restore() - - def __plot__(self, context, bbox, palette, *args, **kwds): - """Draws the dendrogram on the given Cairo context + def __plot__(self, backend, context, *args, **kwds): + """Draws the dendrogram on the given Cairo context or matplotlib Axes. Supported keyword arguments are: @@ -776,131 +741,18 @@ def __plot__(self, context, bbox, palette, *args, **kwds): The default is C{left-right}. """ - from igraph.layout import Layout - - if self._names is None: - self._names = [str(x) for x in range(self._nitems)] - - orientation = str_to_orientation( - kwds.get("orientation", "lr"), reversed_vertical=True - ) - horiz = orientation in ("lr", "rl") - - # Get the font height - font_height = context.font_extents()[2] - - # Calculate space needed for individual items at the - # bottom of the dendrogram - item_boxes = [ - self._item_box_size(context, horiz, idx) for idx in range(self._nitems) - ] - - # Small correction for cases when the right edge of the labels is - # aligned with the tips of the dendrogram branches - ygap = 2 if orientation == "bt" else 0 - xgap = 2 if orientation == "lr" else 0 - item_boxes = [(x + xgap, y + ygap) for x, y in item_boxes] - - # Calculate coordinates - layout = Layout([(0, 0)] * self._nitems, dim=2) - inorder = self._traverse_inorder() - if not horiz: - x, y = 0, 0 - for idx, element in enumerate(inorder): - layout[element] = (x, 0) - x += max(font_height, item_boxes[element][0]) - - for id1, id2 in self._merges: - y += 1 - layout.append(((layout[id1][0] + layout[id2][0]) / 2.0, y)) - - # Mirror or rotate the layout if necessary - if orientation == "bt": - layout.mirror(1) + if backend == "matplotlib": + drawer = MatplotlibDendrogramDrawer(context) else: - x, y = 0, 0 - for idx, element in enumerate(inorder): - layout[element] = (0, y) - y += max(font_height, item_boxes[element][1]) - - for id1, id2 in self._merges: - x += 1 - layout.append((x, (layout[id1][1] + layout[id2][1]) / 2.0)) - - # Mirror or rotate the layout if necessary - if orientation == "rl": - layout.mirror(0) - - # Rescale layout to the bounding box - maxw = max(e[0] for e in item_boxes) - maxh = max(e[1] for e in item_boxes) - - # w, h: width and height of the area containing the dendrogram - # tree without the items. - # delta_x, delta_y: displacement of the dendrogram tree - width, height = float(bbox.width), float(bbox.height) - delta_x, delta_y = 0, 0 - if horiz: - width -= maxw - if orientation == "lr": - delta_x = maxw - else: - height -= maxh - if orientation == "tb": - delta_y = maxh + bbox = kwds.pop('bbox', None) + palette = kwds.pop('palette', None) + if bbox is None: + raise ValueError('bbox is required for cairo plots') + if palette is None: + raise ValueError('palette is required for cairo plots') + drawer = CairoDendrogramDrawer(context, bbox, palette) - if horiz: - delta_y += font_height / 2.0 - else: - delta_x += font_height / 2.0 - layout.fit_into( - (delta_x, delta_y, width - delta_x, height - delta_y), - keep_aspect_ratio=False, - ) - - context.save() - - context.translate(bbox.left, bbox.top) - context.set_source_rgb(0.0, 0.0, 0.0) - context.set_line_width(1) - - # Draw items - if horiz: - sgn = 0 if orientation == "rl" else -1 - for idx in range(self._nitems): - x = layout[idx][0] + sgn * item_boxes[idx][0] - y = layout[idx][1] - item_boxes[idx][1] / 2.0 - self._plot_item(context, horiz, idx, x, y) - else: - sgn = 1 if orientation == "bt" else 0 - for idx in range(self._nitems): - x = layout[idx][0] - item_boxes[idx][0] / 2.0 - y = layout[idx][1] + sgn * item_boxes[idx][1] - self._plot_item(context, horiz, idx, x, y) - - # Draw dendrogram lines - if not horiz: - for idx, (id1, id2) in enumerate(self._merges): - x0, y0 = layout[id1] - x1, y1 = layout[id2] - x2, y2 = layout[idx + self._nitems] - context.move_to(x0, y0) - context.line_to(x0, y2) - context.line_to(x1, y2) - context.line_to(x1, y1) - context.stroke() - else: - for idx, (id1, id2) in enumerate(self._merges): - x0, y0 = layout[id1] - x1, y1 = layout[id2] - x2, y2 = layout[idx + self._nitems] - context.move_to(x0, y0) - context.line_to(x2, y0) - context.line_to(x2, y1) - context.line_to(x1, y1) - context.stroke() - - context.restore() + drawer.draw(self, **kwds) @property def merges(self): @@ -1008,8 +860,8 @@ def optimal_count(self): def optimal_count(self, value): self._optimal_count = max(int(value), 1) - def __plot__(self, context, bbox, palette, *args, **kwds): - """Draws the vertex dendrogram on the given Cairo context + def __plot__(self, backend, context, *args, **kwds): + """Draws the vertex dendrogram on the given Cairo context or matplotlib Axes See L{Dendrogram.__plot__} for the list of supported keyword arguments.""" @@ -1025,7 +877,9 @@ class VisualVertexBuilder(AttributeCollectorBase): name if name is not None else str(idx) for idx, name in enumerate(self._names) ] - result = Dendrogram.__plot__(self, context, bbox, palette, *args, **kwds) + result = Dendrogram.__plot__( + self, backend, context, *args, **kwds + ) del self._names return result @@ -1272,9 +1126,8 @@ def subgraphs(self): """ return [self._graph.subgraph(cl) for cl in self] - def __plot__(self, context, bbox, palette, *args, **kwds): - """Plots the cover to the given Cairo context in the given - bounding box. + def __plot__(self, backend, context, *args, **kwds): + """Plots the cover to the given Cairo context or matplotlib Axes. This is done by calling L{Graph.__plot__()} with the same arguments, but drawing nice colored blobs around the vertex groups. @@ -1322,15 +1175,18 @@ def __plot__(self, context, bbox, palette, *args, **kwds): """ if "edge_color" not in kwds and "color" not in self.graph.edge_attributes(): # Set up a default edge coloring based on internal vs external edges - colors = ["grey20", "grey80"] + if backend == 'matplotlib': + colors = ["dimgrey", "silver"] + else: + colors = ["grey20", "grey80"] + kwds["edge_color"] = [ colors[is_crossing] for is_crossing in self.crossing() ] - if "palette" in kwds: - palette = kwds["palette"] - else: - palette = ClusterColoringPalette(len(self)) + palette = kwds.get("palette", None) + if palette is None: + kwds["palette"] = ClusterColoringPalette(len(self)) if "mark_groups" not in kwds: if Configuration.instance()["plotting.mark_groups"]: @@ -1340,7 +1196,7 @@ def __plot__(self, context, bbox, palette, *args, **kwds): kwds["mark_groups"], self ) - return self._graph.__plot__(context, bbox, palette, *args, **kwds) + return self._graph.__plot__(backend, context, *args, **kwds) def _formatted_cluster_iterator(self): """Iterates over the clusters and formats them into a string to be @@ -1447,9 +1303,9 @@ def parents(self): if the given group is the root.""" return self._parent[:] - def __plot__(self, context, bbox, palette, *args, **kwds): - """Plots the cohesive block structure to the given Cairo context in - the given bounding box. + def __plot__(self, backend, context, *args, **kwds): + """Plots the cohesive block structure to the given Cairo context or + matplotlib Axes. Since a L{CohesiveBlocks} instance is also a L{VertexCover}, keyword arguments accepted by L{VertexCover.__plot__()} are also accepted here. @@ -1473,7 +1329,9 @@ def __plot__(self, context, bbox, palette, *args, **kwds): if "vertex_color" not in kwds: kwds["vertex_color"] = self.max_cohesions() - return VertexCover.__plot__(self, context, bbox, palette, *args, **kwds) + return VertexCover.__plot__( + self, backend, context, *args, **kwds + ) def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 43d890219..758c4adfd 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -8,81 +8,10 @@ as well as saving them to and retrieving them from disk. """ -import sys - -if sys.version_info < (3, 2): - from configparser import SafeConfigParser as ConfigParser -else: - from configparser import ConfigParser - -import platform import os.path +import platform - -def get_platform_image_viewer(): - """Returns the path of an image viewer on the given platform. - - Deprecated since python-igraph 0.9.1 and will be removed in 0.10.0. - - @deprecated: This function was only used by the now-deprecated C{show()} - method of the Plot class. - """ - plat = platform.system() - if plat == "Darwin": - # Most likely Mac OS X - return "open" - elif plat == "Linux": - # Linux has a whole lot of choices, try to find one - choices = [ - "eog", - "gthumb", - "gqview", - "kuickshow", - "xnview", - "display", - "gpicview", - "gwenview", - "qiv", - "gimv", - "ristretto", - "geeqie", - "eom", - ] - paths = ["/usr/bin", "/bin"] - for path in paths: - for choice in choices: - full_path = os.path.join(path, choice) - if os.path.isfile(full_path): - return full_path - return "" - elif plat == "FreeBSD": - # FreeBSD also has a whole lot of choices, try to find one - choices = [ - "eog", - "gthumb", - "geeqie", - "display", - "gpicview", - "gwenview", - "qiv", - "gimv", - "ristretto", - "geeqie", - "eom", - ] - paths = ["%%LOCALBASE%%/bin"] - for path in paths: - for choice in choices: - full_path = os.path.join(path, choice) - if os.path.isfile(full_path): - return full_path - return "" - elif plat == "Windows" or plat == "Microsoft": # Thanks to Dale Hunscher - # Use the built-in Windows image viewer, if available - return "start" - else: - # Unknown system - return "" +from configparser import ConfigParser class Configuration: @@ -140,25 +69,15 @@ class Configuration: - B{verbose}: whether L{igraph} should talk more than really necessary. For instance, if set to C{True}, some functions display progress bars. - Application settings - -------------------- - - These settings specify the external applications that are possibly - used by C{igraph}. They are all stored in section C{apps}. - - - B{image_viewer}: image viewer application. If set to an empty string, - it will be determined automatically from the platform C{igraph} runs - on. On Mac OS X, it defaults to the Preview application. On Linux, - it chooses a viewer from several well-known Linux viewers like - C{gthumb}, C{kuickview} and so on (see the source code for the full - list). On Windows, it defaults to the system's built-in image viewer. - Plotting settings ----------------- These settings specify the default values used by plotting functions. They are all stored in section C{plotting}. + - B{backend}: either "cairo" if you want to use Cairo for plotting + or "matplotlib" if you want to use the Matplotlib plotting backend. + - B{layout}: default graph layout algorithm to be used. - B{mark_groups}: whether to mark the clusters by polygons when @@ -246,7 +165,7 @@ def setfloat(obj, section, key, value): _definitions = { "general.shells": {"default": "IPythonShell,ClassicPythonShell"}, "general.verbose": {"default": True, "type": "boolean"}, - "apps.image_viewer": {"default": get_platform_image_viewer()}, + "plotting.backend": {"default": "cairo"}, "plotting.layout": {"default": "auto"}, "plotting.mark_groups": {"default": False, "type": "boolean"}, "plotting.palette": {"default": "gray"}, diff --git a/src/igraph/datatypes.py b/src/igraph/datatypes.py index 1a53bba07..ebae642db 100644 --- a/src/igraph/datatypes.py +++ b/src/igraph/datatypes.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """Additional auxiliary data types""" -from itertools import islice +__all__ = ("Matrix",) class Matrix: @@ -271,8 +271,8 @@ def __iter__(self): the original matrix.""" return (list(row) for row in self._data) - def __plot__(self, context, bbox, palette, **kwds): - """Plots the matrix to the given Cairo context in the given box + def __plot__(self, backend, context, **kwds): + """Plots the matrix to the given Cairo context or matplotlib Axes. Besides the usual self-explanatory plotting parameters (C{context}, C{bbox}, C{palette}), it accepts the following keyword arguments: @@ -322,157 +322,10 @@ def __plot__(self, context, bbox, palette, **kwds): is square-shaped, the same names are used for both column and row names. """ - grid_width = float(kwds.get("grid_width", 1.0)) - border_width = float(kwds.get("border_width", 1.0)) - style = kwds.get("style", "boolean") - row_names = kwds.get("row_names") - col_names = kwds.get("col_names", row_names) - values = kwds.get("values") - value_format = kwds.get("value_format", str) - - # Validations - if style not in ("boolean", "palette", "none", None): - raise ValueError("invalid style") - if style == "none": - style = None - if row_names is None and col_names is not None: - row_names = col_names - if row_names is not None: - row_names = [str(name) for name in islice(row_names, self._nrow)] - if len(row_names) < self._nrow: - row_names.extend([""] * (self._nrow - len(row_names))) - if col_names is not None: - col_names = [str(name) for name in islice(col_names, self._ncol)] - if len(col_names) < self._ncol: - col_names.extend([""] * (self._ncol - len(col_names))) - if values is False: - values = None - if values is True: - values = self - if isinstance(values, list): - values = Matrix(list) - if values is not None and not isinstance(values, Matrix): - raise TypeError("values must be None, False, True or a matrix") - if values is not None and values.shape != self.shape: - raise ValueError("values must be a matrix of size %s" % self.shape) - - # Calculate text extents if needed - if row_names is not None or col_names is not None: - te = context.text_extents - space_width = te(" ")[4] - max_row_name_width = max([te(s)[4] for s in row_names]) + space_width - max_col_name_width = max([te(s)[4] for s in col_names]) + space_width - else: - max_row_name_width, max_col_name_width = 0, 0 - - # Calculate sizes - total_width = float(bbox.width) - max_row_name_width - total_height = float(bbox.height) - max_col_name_width - dx = total_width / self.shape[1] - dy = total_height / self.shape[0] - if kwds.get("square", True): - dx, dy = min(dx, dy), min(dx, dy) - total_width, total_height = dx * self.shape[1], dy * self.shape[0] - ox = bbox.left + (bbox.width - total_width - max_row_name_width) / 2.0 - oy = bbox.top + (bbox.height - total_height - max_col_name_width) / 2.0 - ox += max_row_name_width - oy += max_col_name_width - - # Determine rescaling factors for the palette if needed - if style == "palette": - mi, ma = self.min(), self.max() - color_offset = mi - color_ratio = (len(palette) - 1) / float(ma - mi) - - # Validate grid width - if dx < 3 * grid_width or dy < 3 * grid_width: - grid_width = 0.0 - if grid_width > 0: - context.set_line_width(grid_width) - else: - # When the grid width is zero, we will still stroke the - # rectangles, but with the same color as the fill color - # of the cell - otherwise we would get thin white lines - # between the cells as a drawing artifact - context.set_line_width(1) - - # Draw row names (if any) - context.set_source_rgb(0.0, 0.0, 0.0) - if row_names is not None: - x, y = ox, oy - for heading in row_names: - _, _, _, h, xa, _ = context.text_extents(heading) - context.move_to(x - xa - space_width, y + (dy + h) / 2.0) - context.show_text(heading) - y += dy - - # Draw column names (if any) - if col_names is not None: - context.save() - context.translate(ox, oy) - context.rotate(-1.5707963285) # pi/2 - x, y = 0.0, 0.0 - for heading in col_names: - _, _, _, h, _, _ = context.text_extents(heading) - context.move_to(x + space_width, y + (dx + h) / 2.0) - context.show_text(heading) - y += dx - context.restore() - - # Draw matrix - x, y = ox, oy - if style is None: - fill = lambda: None # noqa: E731 - else: - fill = context.fill_preserve - for row in self: - for item in row: - if item is None: - x += dx - continue - if style == "boolean": - if item: - context.set_source_rgb(0.0, 0.0, 0.0) - else: - context.set_source_rgb(1.0, 1.0, 1.0) - elif style == "palette": - cidx = int((item - color_offset) * color_ratio) - if cidx < 0: - cidx = 0 - context.set_source_rgba(*palette.get(cidx)) - context.rectangle(x, y, dx, dy) - if grid_width > 0: - fill() - context.set_source_rgb(0.5, 0.5, 0.5) - context.stroke() - else: - fill() - context.stroke() - x += dx - x, y = ox, y + dy - - # Draw cell values - if values is not None: - x, y = ox, oy - context.set_source_rgb(0.0, 0.0, 0.0) - for row in values.data: - if hasattr(value_format, "__call__"): - values = [value_format(item) for item in row] - else: - values = [value_format % item for item in row] - for item in values: - th, tw = context.text_extents(item)[3:5] - context.move_to(x + (dx - tw) / 2.0, y + (dy + th) / 2.0) - context.show_text(item) - x += dx - x, y = ox, y + dy - - # Draw borders - if border_width > 0: - context.set_line_width(border_width) - context.set_source_rgb(0.0, 0.0, 0.0) - context.rectangle(ox, oy, dx * self.shape[1], dy * self.shape[0]) - context.stroke() + from igraph.drawing import DrawerDirectory + + drawer = DrawerDirectory.resolve(self, backend)(context) + drawer.draw(self, **kwds) def min(self, dim=None): """Returns the minimum of the matrix along the given dimension @@ -716,7 +569,7 @@ def __setitem__(self, item, value): self._ids[item] = value def __len__(self): - """"Returns the number of items""" + """ "Returns the number of items""" return len(self._ids) def reverse_dict(self): diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 244bb9116..a1f53d00d 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -1,402 +1,119 @@ """ Drawing and plotting routines for IGraph. -Plotting is dependent on the C{pycairo} or C{cairocffi} libraries that provide -Python bindings to the popular U{Cairo library}. +IGraph has two plotting backends at the moment: Cairo and Matplotlib. + +The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that +provide Python bindings to the popular U{Cairo library}. This means that if you don't have U{pycairo} or U{cairocffi} installed, you won't be able -to use the plotting capabilities. However, you can still use L{Graph.write_svg} -to save the graph to an SVG file and view it from +to use the Cairo plotting backend. Whenever the documentation refers to the +C{pycairo} library, you can safely replace it with C{cairocffi} as the two are +API-compatible. + +The Matplotlib backend uses the U{Matplotlib library}. +You will need to install it from PyPI if you want to use the Matplotlib +plotting backend. + +If you do not want to (or cannot) install any of the dependencies outlined +above, you can still save the graph to an SVG file and view it from U{Mozilla Firefox} (free) or edit it in U{Inkscape} (free), U{Skencil} (formerly known as Sketch, also free) or Adobe Illustrator. - -Whenever the documentation refers to the C{pycairo} library, you can safely -replace it with C{cairocffi} as the two are API-compatible. """ +from pathlib import Path from warnings import warn -import os -import platform -import time - -from io import BytesIO from igraph.configuration import Configuration +from igraph.drawing.cairo.utils import find_cairo +from igraph.drawing.matplotlib.utils import find_matplotlib +from igraph.drawing.plotly.utils import find_plotly +from igraph.drawing.cairo.plot import CairoPlot from igraph.drawing.colors import Palette, palettes -from igraph.drawing.graph import DefaultGraphDrawer, MatplotlibGraphDrawer -from igraph.drawing.utils import ( - BoundingBox, - Point, - Rectangle, - find_cairo, - find_matplotlib, -) -from igraph.utils import _is_running_in_ipython, named_temporary_file - -__all__ = ("BoundingBox", "DefaultGraphDrawer", "Plot", "Point", "Rectangle", "plot") - -cairo = find_cairo() - -##################################################################### +from igraph.drawing.cairo.graph import CairoGraphDrawer +from igraph.drawing.cairo.matrix import CairoMatrixDrawer +from igraph.drawing.cairo.histogram import CairoHistogramDrawer +from igraph.drawing.cairo.palette import CairoPaletteDrawer +from igraph.drawing.matplotlib.graph import MatplotlibGraphDrawer +from igraph.drawing.matplotlib.matrix import MatplotlibMatrixDrawer +from igraph.drawing.matplotlib.histogram import MatplotlibHistogramDrawer +from igraph.drawing.matplotlib.palette import MatplotlibPaletteDrawer +from igraph.drawing.plotly.graph import PlotlyGraphDrawer + +from igraph.drawing.utils import BoundingBox, Point, Rectangle +from igraph.utils import _is_running_in_ipython + +__all__ = ( + "BoundingBox", + "CairoGraphDrawer", + "MatplotlibGraphDrawer", + "DefaultGraphDrawer", + "Plot", + "Point", + "Rectangle", + "plot", + "DrawerDirectory", +) -class Plot: - """Class representing an arbitrary plot - - Every plot has an associated surface object where the plotting is done. The - surface is an instance of C{cairo.Surface}, a member of the C{pycairo} - library. The surface itself provides a unified API to various plotting - targets like SVG files, X11 windows, PostScript files, PNG files and so on. - C{igraph} usually does not know on which surface it is plotting right now, - since C{pycairo} takes care of the actual drawing. Everything that's supported - by C{pycairo} should be supported by this class as well. - - Current Cairo surfaces include: - - - C{cairo.GlitzSurface} -- OpenGL accelerated surface for the X11 - Window System. - - - C{cairo.ImageSurface} -- memory buffer surface. Can be written to a - C{PNG} image file. - - - C{cairo.PDFSurface} -- PDF document surface. - - - C{cairo.PSSurface} -- PostScript document surface. - - - C{cairo.SVGSurface} -- SVG (Scalable Vector Graphics) document surface. - - - C{cairo.Win32Surface} -- Microsoft Windows screen rendering. +# TODO: deprecate +Plot = CairoPlot - - C{cairo.XlibSurface} -- X11 Window System screen rendering. +# TODO: deprecate +DefaultGraphDrawer = CairoGraphDrawer - If you create a C{Plot} object with a string given as the target surface, - the string will be treated as a filename, and its extension will decide - which surface class will be used. Please note that not all surfaces might - be available, depending on your C{pycairo} installation. - A C{Plot} has an assigned default palette (see L{igraph.drawing.colors.Palette}) - which is used for plotting objects. +class DrawerDirectory: + """Static class that finds the object/backend drawer - A C{Plot} object also has a list of objects to be plotted with their - respective bounding boxes, palettes and opacities. Palettes assigned - to an object override the default palette of the plot. Objects can be - added by the L{Plot.add} method and removed by the L{Plot.remove} method. + This directory is used by the __plot__ functions. """ - - def __init__(self, target=None, bbox=None, palette=None, background=None): - """Creates a new plot. - - @param target: the target surface to write to. It can be one of the - following types: - - - C{None} -- an appropriate surface will be created and the object - will be plotted there. - - - C{cairo.Surface} -- the given Cairo surface will be used. - - - C{string} -- a file with the given name will be created and an - appropriate Cairo surface will be attached to it. - - @param bbox: the bounding box of the surface. It is interpreted - differently with different surfaces: PDF and PS surfaces will - treat it as points (1 point = 1/72 inch). Image surfaces will - treat it as pixels. SVG surfaces will treat it as an abstract - unit, but it will mostly be interpreted as pixels when viewing - the SVG file in Firefox. - - @param palette: the palette primarily used on the plot if the - added objects do not specify a private palette. Must be either - an L{igraph.drawing.colors.Palette} object or a string referring - to a valid key of C{igraph.drawing.colors.palettes} (see module - L{igraph.drawing.colors}) or C{None}. In the latter case, the default - palette given by the configuration key C{plotting.palette} is used. - - @param background: the background color. If C{None}, the background - will be transparent. You can use any color specification here that - is understood by L{igraph.drawing.colors.color_name_to_rgba}. - """ - self._filename = None - self._surface_was_created = not isinstance(target, cairo.Surface) - self._need_tmpfile = False - - # Several Windows-specific hacks will be used from now on, thanks - # to Dale Hunscher for debugging and fixing all that stuff - self._windows_hacks = "Windows" in platform.platform() - - if bbox is None: - self.bbox = BoundingBox(600, 600) - elif isinstance(bbox, tuple) or isinstance(bbox, list): - self.bbox = BoundingBox(bbox) - else: - self.bbox = bbox - - if palette is None: - config = Configuration.instance() - palette = config["plotting.palette"] - if not isinstance(palette, Palette): - palette = palettes[palette] - self._palette = palette - - if target is None: - self._need_tmpfile = True - self._surface = cairo.ImageSurface( - cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) - ) - elif isinstance(target, cairo.Surface): - self._surface = target - else: - self._filename = target - _, ext = os.path.splitext(target) - ext = ext.lower() - if ext == ".pdf": - self._surface = cairo.PDFSurface( - target, self.bbox.width, self.bbox.height - ) - elif ext == ".ps" or ext == ".eps": - self._surface = cairo.PSSurface( - target, self.bbox.width, self.bbox.height - ) - elif ext == ".png": - self._surface = cairo.ImageSurface( - cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) - ) - elif ext == ".svg": - self._surface = cairo.SVGSurface( - target, self.bbox.width, self.bbox.height - ) - else: - raise ValueError("image format not handled by Cairo: %s" % ext) - - self._ctx = cairo.Context(self._surface) - self._objects = [] - self._is_dirty = False - - self.background = background - - def add(self, obj, bbox=None, palette=None, opacity=1.0, *args, **kwds): - """Adds an object to the plot. - - Arguments not specified here are stored and passed to the object's - plotting function when necessary. Since you are most likely interested - in the arguments acceptable by graphs, see L{Graph.__plot__} for more - details. - - @param obj: the object to be added - @param bbox: the bounding box of the object. If C{None}, the object - will fill the entire area of the plot. - @param palette: the color palette used for drawing the object. If the - object tries to get a color assigned to a positive integer, it - will use this palette. If C{None}, defaults to the global palette - of the plot. - @param opacity: the opacity of the object being plotted, in the range - 0.0-1.0 - - @see: Graph.__plot__ - """ - if opacity < 0.0 or opacity > 1.0: - raise ValueError("opacity must be between 0.0 and 1.0") - if bbox is None: - bbox = self.bbox - if not isinstance(bbox, BoundingBox): - bbox = BoundingBox(bbox) - self._objects.append((obj, bbox, palette, opacity, args, kwds)) - self.mark_dirty() - - @property - def background(self): - """Returns the background color of the plot. C{None} means a - transparent background. - """ - return self._background - - @background.setter - def background(self, color): - """Sets the background color of the plot. C{None} means a - transparent background. You can use any color specification here - that is understood by the C{get} method of the current palette - or by L{igraph.drawing.colors.color_name_to_rgb}. - """ - if color is None: - self._background = None - else: - self._background = self._palette.get(color) - - def remove(self, obj, bbox=None, idx=1): - """Removes an object from the plot. - - If the object has been added multiple times and no bounding box - was specified, it removes the instance which occurs M{idx}th - in the list of identical instances of the object. - - @param obj: the object to be removed - @param bbox: optional bounding box specification for the object. - If given, only objects with exactly this bounding box will be - considered. - @param idx: if multiple objects match the specification given by - M{obj} and M{bbox}, only the M{idx}th occurrence will be removed. - @return: C{True} if the object has been removed successfully, - C{False} if the object was not on the plot at all or M{idx} - was larger than the count of occurrences - """ - for i in range(len(self._objects)): - current_obj, current_bbox = self._objects[i][0:2] - if current_obj is obj and (bbox is None or current_bbox == bbox): - idx -= 1 - if idx == 0: - self._objects[i : (i + 1)] = [] - self.mark_dirty() - return True - return False - - def mark_dirty(self): - """Marks the plot as dirty (should be redrawn)""" - self._is_dirty = True - - def redraw(self, context=None): - """Redraws the plot""" - ctx = context or self._ctx - if self._background is not None: - ctx.set_source_rgba(*self._background) - ctx.rectangle(0, 0, self.bbox.width, self.bbox.height) - ctx.fill() - - for obj, bbox, palette, opacity, args, kwds in self._objects: - if palette is None: - palette = getattr(obj, "_default_palette", self._palette) - plotter = getattr(obj, "__plot__", None) - if plotter is None: - warn("%s does not support plotting" % (obj,)) - else: - if opacity < 1.0: - ctx.push_group() - else: - ctx.save() - plotter(ctx, bbox, palette, *args, **kwds) - if opacity < 1.0: - ctx.pop_group_to_source() - ctx.paint_with_alpha(opacity) - else: - ctx.restore() - - self._is_dirty = False - - def save(self, fname=None): - """Saves the plot. - - @param fname: the filename to save to. It is ignored if the surface - of the plot is not an C{ImageSurface}. + valid_backends = ('cairo', 'matplotlib') + valid_objects = ( + 'Graph', + 'Matrix', + 'Histogram', + 'Palette', + ) + known_drawers = { + 'cairo': { + 'Graph': CairoGraphDrawer, + 'Matrix': CairoMatrixDrawer, + 'Histogram': CairoHistogramDrawer, + 'Palette': CairoPaletteDrawer, + + }, + 'matplotlib': { + 'Graph': MatplotlibGraphDrawer, + 'Matrix': MatplotlibMatrixDrawer, + 'Histogram': MatplotlibHistogramDrawer, + 'Palette': MatplotlibPaletteDrawer, + }, + 'plotly': { + 'Graph': PlotlyGraphDrawer, + }, + } + + @classmethod + def resolve(cls, obj, backend): + """Given a shape name, returns the corresponding shape drawer class + + @param: obj: an instance of the object to plot + @param backend: the name of the backend + @return: the corresponding shape drawer class + + @raise ValueError: if no drawer is available for this backend/object """ - if self._is_dirty: - self.redraw() - if isinstance(self._surface, cairo.ImageSurface): - if fname is None and self._need_tmpfile: - with named_temporary_file(prefix="igraph", suffix=".png") as fname: - self._surface.write_to_png(fname) - return None - - fname = fname or self._filename - if fname is None: - raise ValueError( - "no file name is known for the surface " + "and none given" - ) - return self._surface.write_to_png(fname) - - if fname is not None: - warn("filename is ignored for surfaces other than ImageSurface") - - self._ctx.show_page() - self._surface.finish() - - def show(self): - """Saves the plot to a temporary file and shows it. - - This method is deprecated from python-igraph 0.9.1 and will be removed in - version 0.10.0. + object_name = str(obj.__class__).split('.')[-1].strip("<'>") - @deprecated: Opening an image viewer with a temporary file never worked - reliably across platforms. - """ - warn("Plot.show() is deprecated from python-igraph 0.9.1", DeprecationWarning) - - if not isinstance(self._surface, cairo.ImageSurface): - sur = cairo.ImageSurface( - cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) - ) - ctx = cairo.Context(sur) - self.redraw(ctx) - else: - sur = self._surface - ctx = self._ctx - if self._is_dirty: - self.redraw(ctx) - - with named_temporary_file(prefix="igraph", suffix=".png") as tmpfile: - sur.write_to_png(tmpfile) - config = Configuration.instance() - imgviewer = config["apps.image_viewer"] - if not imgviewer: - # No image viewer was given and none was detected. This - # should only happen on unknown platforms. - plat = platform.system() - raise NotImplementedError( - "showing plots is not implemented on this platform: %s" % plat + try: + return cls.known_drawers[backend][object_name] + except KeyError: + raise ValueError( + f"unknown drawer for {object_name} and backend {backend}", ) - else: - os.system("%s %s" % (imgviewer, tmpfile)) - if platform.system() == "Darwin" or self._windows_hacks: - # On Mac OS X and Windows, launched applications are likely to - # fork and give control back to Python immediately. - # Chances are that the temporary image file gets removed - # before the image viewer has a chance to open it, so - # we wait here a little bit. Yes, this is quite hackish :( - time.sleep(5) - - def _repr_svg_(self): - """Returns an SVG representation of this plot as a string. - - This method is used by IPython to display this plot inline. - """ - io = BytesIO() - # Create a new SVG surface and use that to get the SVG representation, - # which will end up in io - surface = cairo.SVGSurface(io, self.bbox.width, self.bbox.height) - context = cairo.Context(surface) - # Plot the graph on this context - self.redraw(context) - # No idea why this is needed but python crashes without - context.show_page() - surface.finish() - # Return the raw SVG representation - result = io.getvalue().decode("utf-8") - return result, {"isolated": True} # put it inside an iframe - - @property - def bounding_box(self): - """Returns the bounding box of the Cairo surface as a - L{BoundingBox} object""" - return BoundingBox(self.bbox) - - @property - def height(self): - """Returns the height of the Cairo surface on which the plot - is drawn""" - return self.bbox.height - - @property - def surface(self): - """Returns the Cairo surface on which the plot is drawn""" - return self._surface - - @property - def width(self): - """Returns the width of the Cairo surface on which the plot - is drawn""" - return self.bbox.width - - -##################################################################### def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @@ -424,10 +141,9 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): refer to a PNG image, an arbitrary window, an SVG file, anything that Cairo can handle. - - C{None} -- a temporary file will be created and the object will be - plotted there. igraph will attempt to open an image viewer and show - the temporary file. This feature is deprecated from python-igraph - version 0.9.1 and will be removed in 0.10.0. + - C{None} -- no plotting will be performed; igraph simply returns a + CairoPlot_ object that you can use to manipulate the plot and save it + to a file later. @param bbox: the bounding box of the plot. It must be a tuple with either two or four integers, or a L{BoundingBox} object. If this is a tuple @@ -436,14 +152,14 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): SVG and PostScript plots, where 72 pt = 1 inch = 2.54 cm). If this is a tuple with four integers, the first two denotes the X and Y coordinates of a corner and the latter two denoting the X and Y coordinates of the - opposite corner. + opposite corner. Ignored for Matplotlib plots. @keyword opacity: the opacity of the object being plotted. It can be used to overlap several plots of the same graph if you use the same layout for them -- for instance, you might plot a graph with opacity 0.5 and then plot its spanning tree over it with opacity 0.1. To achieve this, you'll need to modify the L{Plot} object returned with - L{Plot.add}. + L{Plot.add}. Ignored for Matplotlib plots. @keyword palette: the palette primarily used on the plot if the added objects do not specify a private palette. Must be either @@ -455,7 +171,7 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @keyword margin: the top, right, bottom, left margins as a 4-tuple. If it has less than 4 elements or is a single float, the elements will be re-used until the length is at least 4. The default margin - is 20 on each side. + is 20 units on each side. Ignored for Matplotlib plots. @keyword inline: whether to try and show the plot object inline in the current IPython notebook. Passing C{None} here or omitting this keyword @@ -464,55 +180,102 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): argument has an effect only if igraph is run inside IPython and C{target} is C{None}. - @return: an appropriate L{Plot} object. + @return: an appropriate L{CairoPlot} object for the Cairo backend or the + Matplotlib C{Axes} object for the Matplotlib backend @see: Graph.__plot__ """ + + VALID_BACKENDS = ("cairo", "matplotlib", "plotly") + _, plt = find_matplotlib() + cairo = find_cairo() + plotly = find_plotly() + # Switch backend based on target (first) and config (second) if hasattr(plt, "Axes") and isinstance(target, plt.Axes): - result = MatplotlibGraphDrawer(ax=target) - result.draw(obj, *args, **kwds) - return + backend = "matplotlib" + elif hasattr(plotly, "graph_objects") and isinstance( + target, plotly.graph_objects.Figure): + backend = "plotly" + elif hasattr(cairo, "Surface") and isinstance(target, cairo.Surface): + backend = "cairo" + else: + backend = Configuration.instance()["plotting.backend"] - if not isinstance(bbox, BoundingBox): - bbox = BoundingBox(bbox) + if backend not in VALID_BACKENDS: + raise ValueError("unknown plotting backend: {0!r}".format(backend)) - result = Plot(target, bbox, background=kwds.get("background", "white")) + if backend in ("matplotlib", "plotly"): + # Choose palette + # If explicit, use it. If not or None, ask the object: None is an + # acceptable response from the object (e.g. for clusterings), it means + # the palette is handled internally. If no response, default to config. + palette = kwds.pop("palette", None) + if palette is None: + palette = getattr( + obj, + "_default_palette", + Configuration.instance()["plotting.palette"], + ) + if palette is not None and not isinstance(palette, Palette): + palette = palettes[palette] - if "margin" in kwds: - bbox = bbox.contract(kwds["margin"]) - del kwds["margin"] - else: - bbox = bbox.contract(20) - result.add(obj, bbox, *args, **kwds) + if target is None: + if backend == "matplotlib": + # Create a new axes if needed + _, target = plt.subplots() + + elif backend == "plotly": + # Create a new figure if needed + target = plotly.graph_objects.Figure() + + # Get the plotting function from the object + plotter = getattr(obj, "__plot__", None) + if plotter is None: + warn("%s does not support plotting" % (obj,)) + return + else: + plotter( + backend, + target, + palette=palette, + *args, + **kwds, + ) + return target + # Cairo backend + inline = False if target is None and _is_running_in_ipython(): - # Get the default value of the `inline` argument from the configuration if - # needed inline = kwds.get("inline") if inline is None: - config = Configuration.instance() - inline = config["shell.ipython.inlining.Plot"] - - # If we requested an inline plot, just return the result and IPython will - # call its _repr_svg_ method. If we requested a non-inline plot, show the - # plot in a separate window and return nothing - if inline: - return result - else: - result.show() - return + inline = Configuration.instance()["shell.ipython.inlining.Plot"] + + palette = kwds.pop("palette", None) + background = kwds.pop("background", "white") + margin = float(kwds.pop("margin", 20)) + result = CairoPlot( + target=target, + bbox=bbox, + palette=palette, + background=background, + ) + item_bbox = result.bbox.contract(margin) + result.add(obj, item_bbox, *args, **kwds) + + # If we requested an inline plot, just return the result and IPython will + # call its _repr_svg_ method. If we requested a non-inline plot, show the + # plot in a separate window and return nothing + if inline: + return result # We are either not in IPython or the user specified an explicit plot target, # so just show or save the result - if target is None: - result.show() - elif isinstance(target, str): + + if isinstance(target, (str, Path)): + # save result.save() # Also return the plot itself return result - - -##################################################################### diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index d434f7b75..e786b064d 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -2,91 +2,22 @@ Abstract base classes for the drawing routines. """ -from igraph.drawing.utils import BoundingBox -from math import pi - -##################################################################### - - -class AbstractDrawer: - """Abstract class that serves as a base class for anything that - draws an igraph object.""" - - def draw(self, *args, **kwds): - """Abstract method, must be implemented in derived classes.""" - raise NotImplementedError("abstract class") +from abc import ABCMeta, abstractmethod +from math import atan2, pi +from .text import TextAlignment ##################################################################### -class AbstractCairoDrawer(AbstractDrawer): +class AbstractDrawer(metaclass=ABCMeta): """Abstract class that serves as a base class for anything that - draws on a Cairo context within a given bounding box. - - A subclass of L{AbstractCairoDrawer} is guaranteed to have an - attribute named C{context} that represents the Cairo context - to draw on, and an attribute named C{bbox} for the L{BoundingBox} - of the drawing area. - """ - - def __init__(self, context, bbox): - """Constructs the drawer and associates it to the given - Cairo context and the given L{BoundingBox}. - - @param context: the context on which we will draw - @param bbox: the bounding box within which we will draw. - Can be anything accepted by the constructor - of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple - or a L{BoundingBox} object). - """ - self.context = context - self._bbox = None - self.bbox = bbox - - @property - def bbox(self): - """The bounding box of the drawing area where this drawer will - draw.""" - return self._bbox - - @bbox.setter - def bbox(self, bbox): - """Sets the bounding box of the drawing area where this drawer - will draw.""" - if not isinstance(bbox, BoundingBox): - self._bbox = BoundingBox(bbox) - else: - self._bbox = bbox + draws an igraph object.""" + @abstractmethod def draw(self, *args, **kwds): """Abstract method, must be implemented in derived classes.""" - raise NotImplementedError("abstract class") - - def _mark_point(self, x, y, color=0, size=4): - """Marks the given point with a small circle on the canvas. - Used primarily for debugging purposes. - - @param x: the X coordinate of the point to mark - @param y: the Y coordinate of the point to mark - @param color: the color of the marker. It can be a - 3-tuple (RGB components, alpha=0.5), a 4-tuple - (RGBA components) or an index where zero means red, 1 means - green, 2 means blue and so on. - @param size: the diameter of the marker. - """ - if isinstance(color, int): - colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (0, 1, 1), (1, 0, 1)] - color = colors[color % len(colors)] - if len(color) == 3: - color += (0.5,) - - ctx = self.context - ctx.save() - ctx.set_source_rgba(*color) - ctx.arc(x, y, size / 2.0, 0, 2 * pi) - ctx.fill() - ctx.restore() + raise NotImplementedError ##################################################################### @@ -142,3 +73,255 @@ def _resolve_hostname(url): url_parts = list(url_parts) url_parts[1] = hostname return urlunparse(url_parts) + + +##################################################################### + + +class AbstractEdgeDrawer(metaclass=ABCMeta): + """Abstract edge drawer object from which all concrete edge drawer + implementations are derived. + """ + + @staticmethod + def _curvature_to_float(value): + """Converts values given to the 'curved' edge style argument + in plotting calls to floating point values.""" + if value is None or value is False: + return 0.0 + if value is True: + return 0.5 + return float(value) + + @abstractmethod + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + """Draws a directed edge. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param src_vertex: the source vertex. Visual properties are defined + by the attributes of this object. + @param dest_vertex: the source vertex. Visual properties are defined + by the attributes of this object. + """ + raise NotImplementedError + + @abstractmethod + def draw_undirected_edge(self, edge, src_vertex, dest_vertex): + """Draws an undirected edge. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param src_vertex: the source vertex. Visual properties are defined + by the attributes of this object. + @param dest_vertex: the source vertex. Visual properties are defined + by the attributes of this object. + """ + raise NotImplementedError + + def get_label_position(self, edge, src_vertex, dest_vertex): + """Returns the position where the label of an edge should be drawn. The + default implementation returns the midpoint of the edge and an alignment + that tries to avoid overlapping the label with the edge. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param src_vertex: the source vertex. Visual properties are given + again as attributes. + @param dest_vertex: the target vertex. Visual properties are given + again as attributes. + @return: a tuple containing two more tuples: the desired position of the + label and the desired alignment of the label, where the position is + given as C{(x, y)} and the alignment is given as C{(horizontal, vertical)}. + Members of the alignment tuple are taken from constants in the + L{TextAlignment} class. + """ + # TODO: curved edges don't play terribly well with this function, + # we could try to get the mid point of the actual curved arrow + # (Bezier curve) and use that. + + # Determine the angle of the line + dx = dest_vertex.position[0] - src_vertex.position[0] + dy = dest_vertex.position[1] - src_vertex.position[1] + if dx != 0 or dy != 0: + # Note that we use -dy because the Y axis points downwards + angle = atan2(-dy, dx) % (2 * pi) + else: + angle = None + + # Determine the midpoint + pos = ( + (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, + (src_vertex.position[1] + dest_vertex.position[1]) / 2, + ) + + # Determine the alignment based on the angle + pi4 = pi / 4 + if angle is None: + halign, valign = TextAlignment.CENTER, TextAlignment.CENTER + else: + index = int((angle / pi4) % 8) + halign = [ + TextAlignment.RIGHT, + TextAlignment.RIGHT, + TextAlignment.RIGHT, + TextAlignment.RIGHT, + TextAlignment.LEFT, + TextAlignment.LEFT, + TextAlignment.LEFT, + TextAlignment.LEFT, + ][index] + valign = [ + TextAlignment.BOTTOM, + TextAlignment.CENTER, + TextAlignment.CENTER, + TextAlignment.TOP, + TextAlignment.TOP, + TextAlignment.CENTER, + TextAlignment.CENTER, + TextAlignment.BOTTOM, + ][index] + + return pos, (halign, valign) + + +##################################################################### + + +class AbstractVertexDrawer(AbstractDrawer): + """Abstract vertex drawer object from which all concrete vertex drawer + implementations are derived.""" + + def __init__(self, palette, layout): + """Constructs the vertex drawer and associates it to the given + palette. + + @param palette: the palette that can be used to map integer + color indices to colors when drawing vertices + @param layout: the layout of the vertices in the graph being drawn + """ + self.layout = layout + self.palette = palette + + @abstractmethod + def draw(self, visual_vertex, vertex, coords): + """Draws the given vertex. + + @param visual_vertex: object specifying the visual properties of the + vertex. Its structure is defined by the VisualVertexBuilder of the + L{CairoGraphDrawer}; see its source code. + @param vertex: the raw igraph vertex being drawn + @param coords: the X and Y coordinates of the vertex as specified by the + layout algorithm, scaled into the bounding box. + """ + raise NotImplementedError + + +##################################################################### + + +class AbstractGraphDrawer(AbstractDrawer): + """Abstract class that serves as a base class for anything that + draws an igraph.Graph. + """ + + @abstractmethod + def draw(self, graph, *args, **kwds): + """Abstract method, must be implemented in derived classes.""" + raise NotImplementedError + + @staticmethod + def ensure_layout(layout, graph=None): + """Helper method that ensures that I{layout} is an instance + of L{Layout}. If it is not, the method will try to convert + it to a L{Layout} according to the following rules: + + - If I{layout} is a string, it is assumed to be a name + of an igraph layout, and it will be passed on to the + C{layout} method of the given I{graph} if I{graph} is + not C{None}. + + - If I{layout} is C{None}, the C{layout} method of + I{graph} will be invoked with no parameters, which + will call the default layout algorithm. + + - Otherwise, I{layout} will be passed on to the constructor + of L{Layout}. This handles lists of lists, lists of tuples + and such. + + If I{layout} is already a L{Layout} instance, it will still + be copied and a copy will be returned. This is because graph + drawers are allowed to transform the layout for their purposes, + and we don't want the transformation to propagate back to the + caller. + """ + from igraph.layout import Layout # avoid circular imports + + if isinstance(layout, Layout): + layout = Layout(layout.coords) + elif isinstance(layout, str) or layout is None: + layout = graph.layout(layout) + else: + layout = Layout(layout) + + return layout + + @staticmethod + def _determine_edge_order(graph, kwds): + """Returns the order in which the edge of the given graph have to be + drawn, assuming that the relevant keyword arguments (C{edge_order} and + C{edge_order_by}) are given in C{kwds} as a dictionary. If neither + C{edge_order} nor C{edge_order_by} is present in C{kwds}, this + function returns C{None} to indicate that the graph drawer is free to + choose the most convenient edge ordering.""" + if "edge_order" in kwds: + # Edge order specified explicitly + return kwds["edge_order"] + + if kwds.get("edge_order_by") is None: + # No edge order specified + return None + + # Order edges by the value of some attribute + edge_order_by = kwds["edge_order_by"] + reverse = False + if isinstance(edge_order_by, tuple): + edge_order_by, reverse = edge_order_by + if isinstance(reverse, str): + reverse = reverse.lower().startswith("desc") + attrs = graph.es[edge_order_by] + edge_order = sorted( + list(range(len(attrs))), key=attrs.__getitem__, reverse=bool(reverse) + ) + + return edge_order + + @staticmethod + def _determine_vertex_order(graph, kwds): + """Returns the order in which the vertices of the given graph have to be + drawn, assuming that the relevant keyword arguments (C{vertex_order} and + C{vertex_order_by}) are given in C{kwds} as a dictionary. If neither + C{vertex_order} nor C{vertex_order_by} is present in C{kwds}, this + function returns C{None} to indicate that the graph drawer is free to + choose the most convenient vertex ordering.""" + if "vertex_order" in kwds: + # Vertex order specified explicitly + return kwds["vertex_order"] + + if kwds.get("vertex_order_by") is None: + # No vertex order specified + return None + + # Order vertices by the value of some attribute + vertex_order_by = kwds["vertex_order_by"] + reverse = False + if isinstance(vertex_order_by, tuple): + vertex_order_by, reverse = vertex_order_by + if isinstance(reverse, str): + reverse = reverse.lower().startswith("desc") + attrs = graph.vs[vertex_order_by] + vertex_order = sorted( + list(range(len(attrs))), key=attrs.__getitem__, reverse=bool(reverse) + ) + + return vertex_order diff --git a/src/igraph/drawing/cairo/__init__.py b/src/igraph/drawing/cairo/__init__.py new file mode 100644 index 000000000..14142ca42 --- /dev/null +++ b/src/igraph/drawing/cairo/__init__.py @@ -0,0 +1,3 @@ +from .plot import CairoPlot + +__all__ = ("CairoPlot",) diff --git a/src/igraph/drawing/cairo/base.py b/src/igraph/drawing/cairo/base.py new file mode 100644 index 000000000..49ccf358b --- /dev/null +++ b/src/igraph/drawing/cairo/base.py @@ -0,0 +1,84 @@ +from math import pi +from typing import Tuple, Union + +from igraph.drawing.baseclasses import AbstractDrawer +from igraph.drawing.utils import BoundingBox + +__all__ = ("AbstractCairoDrawer",) + + +class AbstractCairoDrawer(AbstractDrawer): + """Abstract class that serves as a base class for anything that + draws on a Cairo context within a given bounding box. + + A subclass of L{AbstractCairoDrawer} is guaranteed to have an + attribute named C{context} that represents the Cairo context + to draw on, and an attribute named C{bbox} for the L{BoundingBox} + of the drawing area. + """ + + _bbox: BoundingBox + + def __init__(self, context, bbox: BoundingBox or None): + """Constructs the drawer and associates it to the given + Cairo context and the given L{BoundingBox}. + + @param context: the context on which we will draw + @param bbox: the bounding box within which we will draw. + Can be anything accepted by the constructor + of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple + or a L{BoundingBox} object). + """ + self.context = context + self._bbox = None # type: ignore + # can be set at drawing time + if bbox is not None: + self.bbox = bbox + + @property + def bbox(self) -> BoundingBox: + """The bounding box of the drawing area where this drawer will + draw.""" + return self._bbox + + @bbox.setter + def bbox(self, bbox): + """Sets the bounding box of the drawing area where this drawer + will draw.""" + if not isinstance(bbox, BoundingBox): + self._bbox = BoundingBox(bbox) + else: + self._bbox = bbox + + def _mark_point( + self, + x: float, + y: float, + color: Union[int, Tuple[float, ...]] = 0, + size: float = 4, + ) -> None: + """Marks the given point with a small circle on the canvas. + Used primarily for debugging purposes. + + @param x: the X coordinate of the point to mark + @param y: the Y coordinate of the point to mark + @param color: the color of the marker. It can be a + 3-tuple (RGB components, alpha=0.5), a 4-tuple + (RGBA components) or an index where zero means red, 1 means + green, 2 means blue and so on. + @param size: the diameter of the marker. + """ + if isinstance(color, int): + colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 0), (0, 1, 1), (1, 0, 1)] + color_tuple = colors[color % len(colors)] + elif len(color) == 3: + color_tuple = color + (0.5,) + else: + color_tuple = color + + ctx = self.context + ctx.save() + ctx.set_source_rgba(*color_tuple) + ctx.arc(x, y, size / 2.0, 0, 2 * pi) + ctx.fill() + ctx.restore() diff --git a/src/igraph/drawing/coord.py b/src/igraph/drawing/cairo/coord.py similarity index 85% rename from src/igraph/drawing/coord.py rename to src/igraph/drawing/cairo/coord.py index fc0626bd8..bebdd69fa 100644 --- a/src/igraph/drawing/coord.py +++ b/src/igraph/drawing/cairo/coord.py @@ -2,7 +2,9 @@ Coordinate systems and related plotting routines """ -from igraph.drawing.baseclasses import AbstractCairoDrawer +from abc import abstractmethod + +from igraph.drawing.cairo.base import AbstractCairoDrawer from igraph.drawing.utils import BoundingBox ##################################################################### @@ -18,29 +20,21 @@ class CoordinateSystem(AbstractCairoDrawer): implement an own coordinate system not present in igraph yet. """ - def __init__(self, context, bbox): - """Initializes the coordinate system. - - @param context: the context on which the coordinate system will - be drawn. - @param bbox: the bounding box that will contain the coordinate - system. - """ - AbstractCairoDrawer.__init__(self, context, bbox) - + @abstractmethod def draw(self): """Draws the coordinate system. This method must be overridden in derived classes. """ - raise NotImplementedError("abstract class") + raise NotImplementedError + @abstractmethod def local_to_context(self, x, y): """Converts local coordinates to the context coordinate system (given by the bounding box). This method must be overridden in derived classes.""" - raise NotImplementedError("abstract class") + raise NotImplementedError class DescartesCoordinateSystem(CoordinateSystem): @@ -59,7 +53,7 @@ def __init__(self, context, bbox, bounds): self._sx, self._sy = None, None self._ox, self._oy, self._ox2, self._oy2 = None, None, None, None - CoordinateSystem.__init__(self, context, bbox) + super().__init__(context, bbox) self.bbox = bbox self.bounds = bounds diff --git a/src/igraph/drawing/cairo/dendrogram.py b/src/igraph/drawing/cairo/dendrogram.py new file mode 100644 index 000000000..27c225ea9 --- /dev/null +++ b/src/igraph/drawing/cairo/dendrogram.py @@ -0,0 +1,247 @@ +"""This module provides a dendrogram drawer for the Cairo backend.""" + +from math import pi + +from igraph.drawing.utils import str_to_orientation + +from .base import AbstractCairoDrawer + +__all__ = ("CairoDendrogramDrawer",) + + +class CairoDendrogramDrawer(AbstractCairoDrawer): + """Default Cairo drawer object for dendrograms.""" + + def __init__(self, context, bbox, palette): + """Constructs the drawer and associates it to the given palette. + + @param context: the context on which we will draw + @param bbox: the bounding box within which we will draw. + Can be anything accepted by the constructor + of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple + or a L{BoundingBox} object). + @param palette: the palette that can be used to map integer + color indices to colors when drawing vertices + """ + AbstractCairoDrawer.__init__(self, context, bbox) + self.palette = palette + + @staticmethod + def _item_box_size(dendro, context, horiz, idx): + """Calculates the amount of space needed for drawing an + individual vertex at the bottom of the dendrogram.""" + if dendro._names is None or dendro._names[idx] is None: + x_bearing, _, _, height, x_advance, _ = context.text_extents("") + else: + x_bearing, _, _, height, x_advance, _ = context.text_extents( + str(dendro._names[idx]) + ) + + if horiz: + return x_advance - x_bearing, height + return height, x_advance - x_bearing + + def _plot_item(self, dendro, context, horiz, idx, x, y): + """Plots a dendrogram item to the given Cairo context + + @param context: the Cairo context we are plotting on + @param horiz: whether the dendrogram is horizontally oriented + @param idx: the index of the item + @param x: the X position of the item + @param y: the Y position of the item + """ + if dendro._names is None or dendro._names[idx] is None: + return + + height = self._item_box_size(dendro, context, True, idx)[1] + if horiz: + context.move_to(x, y + height) + context.show_text(str(dendro._names[idx])) + else: + context.save() + context.translate(x, y) + context.rotate(-pi / 2.0) + context.move_to(0, height) + context.show_text(str(dendro._names[idx])) + context.restore() + + def draw(self, dendro, **kwds): + """Draws the given Dendrogram in a Cairo context. + + @param dendro: the igraph.Dendrogram to plot. + + It accepts the following keyword arguments: + + - C{style}: the style of the plot. C{boolean} is useful for plotting + matrices with boolean (C{True}/C{False} or 0/1) values: C{False} + will be shown with a white box and C{True} with a black box. + C{palette} uses the given palette to represent numbers by colors, + the minimum will be assigned to palette color index 0 and the maximum + will be assigned to the length of the palette. C{None} draws transparent + cell backgrounds only. The default style is C{boolean} (but it may + change in the future). C{None} values in the matrix are treated + specially in both cases: nothing is drawn in the cell corresponding + to C{None}. + + - C{square}: whether the cells of the matrix should be square or not. + Default is C{True}. + + - C{grid_width}: line width of the grid shown on the matrix. If zero or + negative, the grid is turned off. The grid is also turned off if the size + of a cell is less than three times the given line width. Default is C{1}. + Fractional widths are also allowed. + + - C{border_width}: line width of the border drawn around the matrix. + If zero or negative, the border is turned off. Default is C{1}. + + - C{row_names}: the names of the rows + + - C{col_names}: the names of the columns. + + - C{values}: values to be displayed in the cells. If C{None} or + C{False}, no values are displayed. If C{True}, the values come + from the matrix being plotted. If it is another matrix, the + values of that matrix are shown in the cells. In this case, + the shape of the value matrix must match the shape of the + matrix being plotted. + + - C{value_format}: a format string or a callable that specifies how + the values should be plotted. If it is a callable, it must be a + function that expects a single value and returns a string. + Example: C{"%#.2f"} for floating-point numbers with always exactly + two digits after the decimal point. See the Python documentation of + the C{%} operator for details on the format string. If the format + string is not given, it defaults to the C{str} function. + + If only the row names or the column names are given and the matrix + is square-shaped, the same names are used for both column and row + names. + """ + from igraph.layout import Layout + + context = self.context + bbox = self.bbox + + if dendro._names is None: + dendro._names = [str(x) for x in range(dendro._nitems)] + + orientation = str_to_orientation( + kwds.get("orientation", "lr"), reversed_vertical=True + ) + horiz = orientation in ("lr", "rl") + + # Get the font height + font_height = context.font_extents()[2] + + # Calculate space needed for individual items at the + # bottom of the dendrogram + item_boxes = [ + dendro._item_box_size(context, horiz, idx) for idx in range(dendro._nitems) + ] + + # Small correction for cases when the right edge of the labels is + # aligned with the tips of the dendrogram branches + ygap = 2 if orientation == "bt" else 0 + xgap = 2 if orientation == "lr" else 0 + item_boxes = [(x + xgap, y + ygap) for x, y in item_boxes] + + # Calculate coordinates + layout = Layout([(0, 0)] * dendro._nitems, dim=2) + inorder = dendro._traverse_inorder() + if not horiz: + x, y = 0, 0 + for idx, element in enumerate(inorder): + layout[element] = (x, 0) + x += max(font_height, item_boxes[element][0]) + + for id1, id2 in dendro._merges: + y += 1 + layout.append(((layout[id1][0] + layout[id2][0]) / 2.0, y)) + + # Mirror or rotate the layout if necessary + if orientation == "bt": + layout.mirror(1) + else: + x, y = 0, 0 + for idx, element in enumerate(inorder): + layout[element] = (0, y) + y += max(font_height, item_boxes[element][1]) + + for id1, id2 in dendro._merges: + x += 1 + layout.append((x, (layout[id1][1] + layout[id2][1]) / 2.0)) + + # Mirror or rotate the layout if necessary + if orientation == "rl": + layout.mirror(0) + + # Rescale layout to the bounding box + maxw = max(e[0] for e in item_boxes) + maxh = max(e[1] for e in item_boxes) + + # w, h: width and height of the area containing the dendrogram + # tree without the items. + # delta_x, delta_y: displacement of the dendrogram tree + width, height = float(bbox.width), float(bbox.height) + delta_x, delta_y = 0, 0 + if horiz: + width -= maxw + if orientation == "lr": + delta_x = maxw + else: + height -= maxh + if orientation == "tb": + delta_y = maxh + + if horiz: + delta_y += font_height / 2.0 + else: + delta_x += font_height / 2.0 + layout.fit_into( + (delta_x, delta_y, width - delta_x, height - delta_y), + keep_aspect_ratio=False, + ) + + context.save() + + context.translate(bbox.left, bbox.top) + context.set_source_rgb(0.0, 0.0, 0.0) + context.set_line_width(1) + + # Draw items + if horiz: + sgn = 0 if orientation == "rl" else -1 + for idx in range(dendro._nitems): + x = layout[idx][0] + sgn * item_boxes[idx][0] + y = layout[idx][1] - item_boxes[idx][1] / 2.0 + self._plot_item(dendro, context, horiz, idx, x, y) + else: + sgn = 1 if orientation == "bt" else 0 + for idx in range(dendro._nitems): + x = layout[idx][0] - item_boxes[idx][0] / 2.0 + y = layout[idx][1] + sgn * item_boxes[idx][1] + dendro._plot_item(dendro, context, horiz, idx, x, y) + + # Draw dendrogram lines + if not horiz: + for idx, (id1, id2) in enumerate(dendro._merges): + x0, y0 = layout[id1] + x1, y1 = layout[id2] + x2, y2 = layout[idx + dendro._nitems] + context.move_to(x0, y0) + context.line_to(x0, y2) + context.line_to(x1, y2) + context.line_to(x1, y1) + context.stroke() + else: + for idx, (id1, id2) in enumerate(dendro._merges): + x0, y0 = layout[id1] + x1, y1 = layout[id2] + x2, y2 = layout[idx + dendro._nitems] + context.move_to(x0, y0) + context.line_to(x2, y0) + context.line_to(x2, y1) + context.line_to(x1, y1) + context.stroke() + + context.restore() diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/cairo/edge.py similarity index 64% rename from src/igraph/drawing/edge.py rename to src/igraph/drawing/cairo/edge.py index 605b2df5c..68ad84f8c 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/cairo/edge.py @@ -2,49 +2,41 @@ Drawers for various edge styles in graph plots. """ +from math import atan2, cos, pi, sin + +from igraph.drawing.baseclasses import AbstractEdgeDrawer +from igraph.drawing.colors import clamp +from igraph.drawing.metamagic import AttributeCollectorBase +from igraph.drawing.utils import euclidean_distance, intersect_bezier_curve_and_circle + +from .utils import find_cairo + __all__ = ( - "AbstractEdgeDrawer", + "AbstractCairoEdgeDrawer", "AlphaVaryingEdgeDrawer", - "ArrowEdgeDrawer", + "CairoArrowEdgeDrawer", "DarkToLightEdgeDrawer", "LightToDarkEdgeDrawer", "TaperedEdgeDrawer", ) -from igraph.drawing.colors import clamp -from igraph.drawing.metamagic import AttributeCollectorBase -from igraph.drawing.text import TextAlignment -from igraph.drawing.utils import find_cairo -from math import atan2, cos, pi, sin, sqrt - cairo = find_cairo() -class AbstractEdgeDrawer: - """Abstract edge drawer object from which all concrete edge drawer - implementations are derived.""" +class AbstractCairoEdgeDrawer(AbstractEdgeDrawer): + """Cairo-specific abstract edge drawer object.""" def __init__(self, context, palette): """Constructs the edge drawer. @param context: a Cairo context on which the edges will be drawn. - @param palette: the palette that can be used to map integer - color indices to colors when drawing edges + @param palette: the palette that can be used to map integer color + indices to colors when drawing edges """ self.context = context self.palette = palette self.VisualEdgeBuilder = self._construct_visual_edge_builder() - @staticmethod - def _curvature_to_float(value): - """Converts values given to the 'curved' edge style argument - in plotting calls to floating point values.""" - if value is None or value is False: - return 0.0 - if value is True: - return 0.5 - return float(value) - def _construct_visual_edge_builder(self): """Construct the visual edge builder that will collect the visual attributes of an edge when it is being drawn.""" @@ -66,18 +58,6 @@ class VisualEdgeBuilder(AttributeCollectorBase): return VisualEdgeBuilder - def draw_directed_edge(self, edge, src_vertex, dest_vertex): - """Draws a directed edge. - - @param edge: the edge to be drawn. Visual properties of the edge - are defined by the attributes of this object. - @param src_vertex: the source vertex. Visual properties are given - again as attributes. - @param dest_vertex: the target vertex. Visual properties are given - again as attributes. - """ - raise NotImplementedError() - def draw_loop_edge(self, edge, vertex): """Draws a loop edge. @@ -132,69 +112,8 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): ctx.stroke() - def get_label_position(self, edge, src_vertex, dest_vertex): - """Returns the position where the label of an edge should be drawn. The - default implementation returns the midpoint of the edge and an alignment - that tries to avoid overlapping the label with the edge. - - @param edge: the edge to be drawn. Visual properties of the edge - are defined by the attributes of this object. - @param src_vertex: the source vertex. Visual properties are given - again as attributes. - @param dest_vertex: the target vertex. Visual properties are given - again as attributes. - @return: a tuple containing two more tuples: the desired position of the - label and the desired alignment of the label, where the position is - given as C{(x, y)} and the alignment is given as C{(horizontal, vertical)}. - Members of the alignment tuple are taken from constants in the - L{TextAlignment} class. - """ - # Determine the angle of the line - dx = dest_vertex.position[0] - src_vertex.position[0] - dy = dest_vertex.position[1] - src_vertex.position[1] - if dx != 0 or dy != 0: - # Note that we use -dy because the Y axis points downwards - angle = atan2(-dy, dx) % (2 * pi) - else: - angle = None - - # Determine the midpoint - pos = ( - (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, - (src_vertex.position[1] + dest_vertex.position[1]) / 2, - ) - # Determine the alignment based on the angle - pi4 = pi / 4 - if angle is None: - halign, valign = TextAlignment.CENTER, TextAlignment.CENTER - else: - index = int((angle / pi4) % 8) - halign = [ - TextAlignment.RIGHT, - TextAlignment.RIGHT, - TextAlignment.RIGHT, - TextAlignment.RIGHT, - TextAlignment.LEFT, - TextAlignment.LEFT, - TextAlignment.LEFT, - TextAlignment.LEFT, - ][index] - valign = [ - TextAlignment.BOTTOM, - TextAlignment.CENTER, - TextAlignment.CENTER, - TextAlignment.TOP, - TextAlignment.TOP, - TextAlignment.CENTER, - TextAlignment.CENTER, - TextAlignment.BOTTOM, - ][index] - - return pos, (halign, valign) - - -class ArrowEdgeDrawer(AbstractEdgeDrawer): +class CairoArrowEdgeDrawer(AbstractCairoEdgeDrawer): """Edge drawer implementation that draws undirected edges as straight lines and directed edges as arrows. """ @@ -207,65 +126,6 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position (x_src, y_src), (x_dest, y_dest) = src_vertex.position, dest_vertex.position - def bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t): - """Computes the Bezier curve from point (x0,y0) to (x3,y3) - via control points (x1,y1) and (x2,y2) with parameter t. - """ - xt = ( - (1.0 - t) ** 3 * x0 - + 3.0 * t * (1.0 - t) ** 2 * x1 - + 3.0 * t ** 2 * (1.0 - t) * x2 - + t ** 3 * x3 - ) - yt = ( - (1.0 - t) ** 3 * y0 - + 3.0 * t * (1.0 - t) ** 2 * y1 - + 3.0 * t ** 2 * (1.0 - t) * y2 - + t ** 3 * y3 - ) - return xt, yt - - def euclidean_distance(x1, y1, x2, y2): - """Computes the Euclidean distance between points (x1,y1) and (x2,y2).""" - return sqrt((1.0 * x1 - x2) ** 2 + (1.0 * y1 - y2) ** 2) - - def intersect_bezier_circle( - x0, y0, x1, y1, x2, y2, x3, y3, radius, max_iter=10 - ): - """Binary search solver for finding the intersection of a Bezier curve - and a circle centered at the curve's end point. - Returns the x,y of the intersection point. - TODO: implement safeguard to ensure convergence in ALL possible cases. - """ - precision = radius / 20.0 - source_target_distance = euclidean_distance(x0, y0, x3, y3) - radius = float(radius) - t0 = 1.0 - t1 = 1.0 - radius / source_target_distance - - xt1, yt1 = bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) - - distance_t0 = 0 - distance_t1 = euclidean_distance(x3, y3, xt1, yt1) - counter = 0 - while abs(distance_t1 - radius) > precision and counter < max_iter: - if ((distance_t1 - radius) > 0) != ((distance_t0 - radius) > 0): - t_new = (t0 + t1) / 2.0 - else: - if abs(distance_t1 - radius) < abs(distance_t0 - radius): - # If t1 gets us closer to the circumference step in the - # same direction - t_new = t1 + (t1 - t0) / 2.0 - else: - t_new = t1 - (t1 - t0) - t_new = 1 if t_new > 1 else (0 if t_new < 0 else t_new) - t0, t1 = t1, t_new - distance_t0 = distance_t1 - xt1, yt1 = bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) - distance_t1 = euclidean_distance(x3, y3, xt1, yt1) - counter += 1 - return bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t1) - # Draw the edge ctx.set_source_rgba(*edge.color) ctx.set_line_width(edge.width) @@ -286,7 +146,7 @@ def intersect_bezier_circle( # Determine where the edge intersects the circumference of the # vertex shape: Tip of the arrow - x2, y2 = intersect_bezier_circle( + x2, y2 = intersect_bezier_curve_and_circle( x_src, y_src, xc1, yc1, xc2, yc2, x_dest, y_dest, dest_vertex.size / 2.0 ) @@ -386,7 +246,7 @@ def intersect_bezier_circle( ctx.fill() -class TaperedEdgeDrawer(AbstractEdgeDrawer): +class TaperedEdgeDrawer(AbstractCairoEdgeDrawer): """Edge drawer implementation that draws undirected edges as straight lines and directed edges as tapered lines that are wider at the source and narrow at the destination. @@ -427,14 +287,14 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): ctx.fill() -class AlphaVaryingEdgeDrawer(AbstractEdgeDrawer): +class AlphaVaryingEdgeDrawer(AbstractCairoEdgeDrawer): """Edge drawer implementation that draws undirected edges as straight lines and directed edges by varying the alpha value of the specified edge color between the source and the destination. """ - def __init__(self, context, alpha_at_src, alpha_at_dest): - super().__init__(context) + def __init__(self, context, palette, alpha_at_src, alpha_at_dest): + super().__init__(context, palette) self.alpha_at_src = (clamp(float(alpha_at_src), 0.0, 1.0),) self.alpha_at_dest = (clamp(float(alpha_at_dest), 0.0, 1.0),) @@ -468,8 +328,8 @@ class LightToDarkEdgeDrawer(AlphaVaryingEdgeDrawer): interpolated in-between. """ - def __init__(self, context): - super().__init__(context, 0.0, 1.0) + def __init__(self, context, palette): + super().__init__(context, palette, 0.0, 1.0) class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): @@ -480,5 +340,5 @@ class DarkToLightEdgeDrawer(AlphaVaryingEdgeDrawer): interpolated in-between. """ - def __init__(self, context): - super().__init__(context, 1.0, 0.0) + def __init__(self, context, palette): + super().__init__(context, palette, 1.0, 0.0) diff --git a/src/igraph/drawing/cairo/graph.py b/src/igraph/drawing/cairo/graph.py new file mode 100644 index 000000000..d66ce7b36 --- /dev/null +++ b/src/igraph/drawing/cairo/graph.py @@ -0,0 +1,425 @@ +""" +Drawing routines to draw graphs. + +This module contains routines to draw graphs on: + + - Cairo surfaces (L{DefaultGraphDrawer}) + - Matplotlib axes (L{MatplotlibGraphDrawer}) + +It also contains routines to send an igraph graph directly to +(U{Cytoscape}) using the +(U{CytoscapeRPC plugin}), see +L{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the current +network from Cytoscape and convert it to igraph format. +""" + +from math import atan2, cos, pi, sin, tan +from warnings import warn + +from igraph._igraph import convex_hull, VertexSeq +from igraph.configuration import Configuration +from igraph.drawing.baseclasses import AbstractGraphDrawer +from igraph.drawing.text import TextAlignment +from igraph.drawing.utils import Point + +from .base import AbstractCairoDrawer +from .edge import CairoArrowEdgeDrawer +from .polygon import CairoPolygonDrawer +from .text import CairoTextDrawer +from .utils import find_cairo +from .vertex import CairoVertexDrawer + +__all__ = ("CairoGraphDrawer",) + +cairo = find_cairo() + +##################################################################### + + +##################################################################### + + +class AbstractCairoGraphDrawer(AbstractGraphDrawer, AbstractCairoDrawer): + """Abstract base class for graph drawers that draw on a Cairo canvas.""" + + def __init__(self, context, bbox): + """Constructs the graph drawer and associates it to the given + Cairo context and the given L{BoundingBox}. + + @param context: the context on which we will draw + @param bbox: the bounding box within which we will draw. + Can be anything accepted by the constructor + of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple + or a L{BoundingBox} object). + """ + AbstractCairoDrawer.__init__(self, context, bbox) + AbstractGraphDrawer.__init__(self) + + +##################################################################### + + +class CairoGraphDrawer(AbstractCairoGraphDrawer): + """Class implementing the default visualisation of a graph. + + The default visualisation of a graph draws the nodes on a 2D plane + according to a given L{Layout}, then draws a straight or curved + edge between nodes connected by edges. This is the visualisation + used when one invokes the L{plot()} function on a L{Graph} object. + + See L{Graph.__plot__()} for the keyword arguments understood by + this drawer.""" + + def __init__( + self, + context, + bbox=None, + vertex_drawer_factory=CairoVertexDrawer, + edge_drawer_factory=CairoArrowEdgeDrawer, + label_drawer_factory=CairoTextDrawer, + ): + """Constructs the graph drawer and associates it to the given + Cairo context and the given L{BoundingBox}. + + @param context: the context on which we will draw + @param bbox: the bounding box within which we will draw. + Can be anything accepted by the constructor + of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple + or a L{BoundingBox} object). + @param vertex_drawer_factory: a factory method that returns an + L{AbstractCairoVertexDrawer} instance bound to a + given Cairo context. The factory method must take + four parameters: the Cairo context, the bounding + box of the drawing area, the palette to be + used for drawing colored vertices, and the graph layout. + The default vertex drawer is L{DefaultVertexDrawer}. + @param edge_drawer_factory: a factory method that returns an + L{AbstractCairoEdgeDrawer} instance bound to a + given Cairo context. The factory method must take + two parameters: the Cairo context and the palette + to be used for drawing colored edges. You can use + any of the actual L{AbstractEdgeDrawer} + implementations here to control the style of + edges drawn by igraph. The default edge drawer is + L{CairoArrowEdgeDrawer}. + @param label_drawer_factory: a factory method that returns a + L{CairoTextDrawer} instance bound to a given Cairo + context. The method must take one parameter: the + Cairo context. The default label drawer is + L{CairoTextDrawer}. + """ + AbstractCairoGraphDrawer.__init__(self, context, bbox) + self.vertex_drawer_factory = vertex_drawer_factory + self.edge_drawer_factory = edge_drawer_factory + self.label_drawer_factory = label_drawer_factory + + def draw(self, graph, *args, **kwds): + # Positional arguments are not used + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, + ) + + bbox = kwds.pop('bbox', None) + if bbox is None: + raise ValueError('bbox is required for cairo plots') + # Validate it through set/get + self.bbox = bbox + bbox = self.bbox + + # Some abbreviations for sake of simplicity + directed = graph.is_directed() + context = self.context + + # Palette + palette = kwds.pop("palette", None) + + # Calculate/get the layout of the graph + layout = self.ensure_layout(kwds.get("layout", None), graph) + + # Determine the size of the margin on each side + margin = kwds.get("margin", 0) + try: + margin = list(margin) # type: ignore + except TypeError: + margin = [margin] + while len(margin) < 4: + margin.extend(margin) + + # Contract the drawing area by the margin and fit the layout + bbox = self.bbox.contract(margin) + layout.fit_into(bbox, keep_aspect_ratio=kwds.get("keep_aspect_ratio", False)) + + # Decide whether we need to calculate the curvature of edges + # automatically -- and calculate them if needed. + autocurve = kwds.get("autocurve", None) + if autocurve or ( + autocurve is None + and "edge_curved" not in kwds + and "curved" not in graph.edge_attributes() + and graph.ecount() < 10000 + ): + from igraph import autocurve + + default = kwds.get("edge_curved", 0) + if default is True: + default = 0.5 + default = float(default) + kwds["edge_curved"] = autocurve(graph, attribute=None, default=default) + + # Construct the vertex, edge and label drawers + vertex_drawer = self.vertex_drawer_factory(context, bbox, palette, layout) + edge_drawer = self.edge_drawer_factory(context, palette) + label_drawer = self.label_drawer_factory(context) + + # Construct the visual vertex/edge builders based on the specifications + # provided by the vertex_drawer and the edge_drawer + vertex_builder = vertex_drawer.VisualVertexBuilder(graph.vs, kwds) + edge_builder = edge_drawer.VisualEdgeBuilder(graph.es, kwds) + + # Determine the order in which we will draw the vertices and edges + vertex_order = self._determine_vertex_order(graph, kwds) + edge_order = self._determine_edge_order(graph, kwds) + + # Draw the highlighted groups (if any) + if "mark_groups" in kwds: + mark_groups = kwds["mark_groups"] + + # Deferred import to avoid a cycle in the import graph + from igraph.clustering import VertexClustering, VertexCover + + # Figure out what to do with mark_groups in order to be able to + # iterate over it and get memberlist-color pairs + if isinstance(mark_groups, dict): + # Dictionary mapping vertex indices or tuples of vertex + # indices to colors + group_iter = iter(mark_groups.items()) + elif isinstance(mark_groups, (VertexClustering, VertexCover)): + # Vertex clustering + group_iter = ((group, color) for color, group in enumerate(mark_groups)) + elif hasattr(mark_groups, "__iter__"): + # Lists, tuples, iterators etc + group_iter = iter(mark_groups) + else: + # False + group_iter = iter({}.items()) + + # We will need a polygon drawer to draw the convex hulls + polygon_drawer = CairoPolygonDrawer(context, bbox) + + # Iterate over color-memberlist pairs + for group, color_id in group_iter: + if not group or color_id is None: + continue + + color = palette.get(color_id) + + if isinstance(group, VertexSeq): + group = [vertex.index for vertex in group] + if not hasattr(group, "__iter__"): + raise TypeError("group membership list must be iterable") + + # Get the vertex indices that constitute the convex hull + hull = [group[i] for i in convex_hull([layout[idx] for idx in group])] + + # Calculate the preferred rounding radius for the corners + corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) + + # Construct the polygon + polygon = [layout[idx] for idx in hull] + + if len(polygon) == 2: + # Expand the polygon (which is a flat line otherwise) + a, b = Point(*polygon[0]), Point(*polygon[1]) + c = corner_radius * (a - b).normalized() + n = Point(-c[1], c[0]) + polygon = [a + n, b + n, b - c, b - n, a - n, a + c] + else: + # Expand the polygon around its center of mass + center = Point( + *[sum(coords) / float(len(coords)) for coords in zip(*polygon)] + ) + polygon = [ + Point(*point).towards(center, -corner_radius) + for point in polygon + ] + + # Draw the hull + context.set_source_rgba(color[0], color[1], color[2], color[3] * 0.25) + polygon_drawer.draw_path(polygon, corner_radius=corner_radius) + context.fill_preserve() + context.set_source_rgba(*color) + context.stroke() + + # Construct the iterator that we will use to draw the edges + es = graph.es + if edge_order is None: + # Default edge order + edge_coord_iter = zip(es, edge_builder) + else: + # Specified edge order + edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) + + # Draw the edges + if directed: + drawer_method = edge_drawer.draw_directed_edge + else: + drawer_method = edge_drawer.draw_undirected_edge + for edge, visual_edge in edge_coord_iter: + src, dest = edge.tuple + src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] + drawer_method(visual_edge, src_vertex, dest_vertex) + + # Construct the iterator that we will use to draw the vertices + vs = graph.vs + if vertex_order is None: + # Default vertex order + vertex_coord_iter = zip(vs, vertex_builder, layout) + else: + # Specified vertex order + vertex_coord_iter = ( + (vs[i], vertex_builder[i], layout[i]) for i in vertex_order + ) + + # Draw the vertices + drawer_method = vertex_drawer.draw + context.set_line_width(1) + for vertex, visual_vertex, coords in vertex_coord_iter: + drawer_method(visual_vertex, vertex, coords) + + # Decide whether the labels have to be wrapped + wrap = kwds.get("wrap_labels") + if wrap is None: + wrap = Configuration.instance()["plotting.wrap_labels"] + wrap = bool(wrap) + + # Construct the iterator that we will use to draw the vertex labels + if vertex_order is None: + # Default vertex order + vertex_coord_iter = zip(vertex_builder, layout) + else: + # Specified vertex order + vertex_coord_iter = ((vertex_builder[i], layout[i]) for i in vertex_order) + + # Draw the vertex labels + for vertex, coords in vertex_coord_iter: + if vertex.label is None: + continue + + # Set the font family, size, color and text + context.select_font_face( + vertex.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL + ) + context.set_font_size(vertex.label_size) + context.set_source_rgba(*vertex.label_color) + label_drawer.text = vertex.label + + if vertex.label_dist: + # Label is displaced from the center of the vertex. + _, yb, w, h, _, _ = label_drawer.text_extents() + w, h = w / 2.0, h / 2.0 + radius = vertex.label_dist * vertex.size / 2.0 + # First we find the reference point that is at distance `radius' + # from the vertex in the direction given by `label_angle'. + # Then we place the label in a way that the line connecting the + # center of the bounding box of the label with the center of the + # vertex goes through the reference point and the reference + # point lies exactly on the bounding box of the vertex. + alpha = vertex.label_angle % (2 * pi) + cx = coords[0] + radius * cos(alpha) + cy = coords[1] - radius * sin(alpha) + # Now we have the reference point. We have to decide which side + # of the label box will intersect with the line that connects + # the center of the label with the center of the vertex. + if w > 0: + beta = atan2(h, w) % (2 * pi) + else: + beta = pi / 2.0 + gamma = pi - beta + if alpha > 2 * pi - beta or alpha <= beta: + # Intersection at left edge of label + cx += w + cy -= tan(alpha) * w + elif alpha > beta and alpha <= gamma: + # Intersection at bottom edge of label + try: + cx += h / tan(alpha) + except Exception: + pass # tan(alpha) == inf + cy -= h + elif alpha > gamma and alpha <= gamma + 2 * beta: + # Intersection at right edge of label + cx -= w + cy += tan(alpha) * w + else: + # Intersection at top edge of label + try: + cx -= h / tan(alpha) + except Exception: + pass # tan(alpha) == inf + cy += h + # Draw the label + label_drawer.draw_at(cx - w, cy - h - yb, wrap=wrap) + else: + # Label is exactly in the center of the vertex + cx, cy = coords + half_size = vertex.size / 2.0 + label_drawer.bbox = ( + cx - half_size, + cy - half_size, + cx + half_size, + cy + half_size, + ) + label_drawer.draw(wrap=wrap) + + # Construct the iterator that we will use to draw the edge labels + es = graph.es + if edge_order is None: + # Default edge order + edge_coord_iter = zip(es, edge_builder) + else: + # Specified edge order + edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) + + # Draw the edge labels + for edge, visual_edge in edge_coord_iter: + if visual_edge.label is None: + continue + + # Set the font family, size, color and text + context.select_font_face( + visual_edge.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL + ) + context.set_font_size(visual_edge.label_size) + context.set_source_rgba(*visual_edge.label_color) + label_drawer.text = visual_edge.label + + # Ask the edge drawer to propose an anchor point for the label + src, dest = edge.tuple + src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] + (x, y), (halign, valign) = edge_drawer.get_label_position( + edge, src_vertex, dest_vertex + ) + + # Measure the text + _, yb, w, h, _, _ = label_drawer.text_extents() + w /= 2.0 + h /= 2.0 + + # Place the text relative to the edge + if halign == TextAlignment.RIGHT: + x -= w + elif halign == TextAlignment.LEFT: + x += w + if valign == TextAlignment.BOTTOM: + y -= h - yb / 2.0 + elif valign == TextAlignment.TOP: + y += h + + # Draw the edge label + label_drawer.halign = halign + label_drawer.valign = valign + label_drawer.bbox = (x - w, y - h, x + w, y + h) + label_drawer.draw(wrap=wrap) diff --git a/src/igraph/drawing/cairo/histogram.py b/src/igraph/drawing/cairo/histogram.py new file mode 100644 index 000000000..de984fe8e --- /dev/null +++ b/src/igraph/drawing/cairo/histogram.py @@ -0,0 +1,59 @@ +"""This module provides implementation for a Cairo-specific histogram drawer""" + +from igraph.drawing.cairo.base import AbstractCairoDrawer + +__all__ = ("CairoHistogramDrawer",) + + +class CairoHistogramDrawer(AbstractCairoDrawer): + """Default Cairo drawer object for histograms""" + + def __init__(self, context): + """Constructs the vertex drawer and associates it to the given + palette. + + @param context: the context on which we will draw + """ + super().__init__(context, bbox=None) + + def draw(self, histogram, **kwds): + """TODO""" + from igraph.drawing.cairo.coord import DescartesCoordinateSystem + + context = self.context + + bbox = self.bbox = kwds.pop('bbox', None) + if bbox is None: + raise ValueError('bbox is required for cairo plots') + + xmin = kwds.get("min", self._min) + ymin = 0 + xmax = kwds.get("max", self._max) + ymax = kwds.get("max_value", max(self._bins)) + width = self._bin_width + + coord_system = DescartesCoordinateSystem( + context, + bbox, + (xmin, ymin, xmax, ymax), + ) + + # Draw the boxes + context.set_line_width(1) + context.set_source_rgb(1.0, 0.0, 0.0) + x = self._min + for value in self._bins: + top_left_x, top_left_y = coord_system.local_to_context(x, value) + x += width + bottom_right_x, bottom_right_y = coord_system.local_to_context(x, 0) + context.rectangle( + top_left_x, + top_left_y, + bottom_right_x - top_left_x, + bottom_right_y - top_left_y, + ) + context.fill() + + # Draw the axes + coord_system.draw() + diff --git a/src/igraph/drawing/cairo/matrix.py b/src/igraph/drawing/cairo/matrix.py new file mode 100644 index 000000000..a5e506bda --- /dev/null +++ b/src/igraph/drawing/cairo/matrix.py @@ -0,0 +1,250 @@ +"""This module provides implementation for a Cairo-specific matrix drawer.""" + +from itertools import islice +from math import pi + +from igraph.drawing.cairo.base import AbstractCairoDrawer + +__all__ = ("CairoMatrixDrawer",) + + +class CairoMatrixDrawer(AbstractCairoDrawer): + """Default Cairo drawer object for matrices.""" + + def __init__(self, context): + """Constructs the vertex drawer and associates it to the given + palette. + + @param context: the context on which we will draw + """ + super().__init__(context, bbox=None) + + def draw(self, matrix, **kwds): + """Draws the given Matrix in a Cairo context. + + @param matrix: the igraph.Matrix to plot. + + It accepts the following keyword arguments: + + - C{bbox}: the bounding box within which we will draw. + Can be anything accepted by the constructor of L{BoundingBox} + (i.e., a 2-tuple, a 4-tuple or a L{BoundingBox} object). + + - C{palette}: the palette that can be used to map integer color + indices to colors when drawing vertices + + - C{style}: the style of the plot. C{boolean} is useful for plotting + matrices with boolean (C{True}/C{False} or 0/1) values: C{False} + will be shown with a white box and C{True} with a black box. + C{palette} uses the given palette to represent numbers by colors, + the minimum will be assigned to palette color index 0 and the maximum + will be assigned to the length of the palette. C{None} draws transparent + cell backgrounds only. The default style is C{boolean} (but it may + change in the future). C{None} values in the matrix are treated + specially in both cases: nothing is drawn in the cell corresponding + to C{None}. + + - C{square}: whether the cells of the matrix should be square or not. + Default is C{True}. + + - C{grid_width}: line width of the grid shown on the matrix. If zero or + negative, the grid is turned off. The grid is also turned off if the size + of a cell is less than three times the given line width. Default is C{1}. + Fractional widths are also allowed. + + - C{border_width}: line width of the border drawn around the matrix. + If zero or negative, the border is turned off. Default is C{1}. + + - C{row_names}: the names of the rows + + - C{col_names}: the names of the columns. + + - C{values}: values to be displayed in the cells. If C{None} or + C{False}, no values are displayed. If C{True}, the values come + from the matrix being plotted. If it is another matrix, the + values of that matrix are shown in the cells. In this case, + the shape of the value matrix must match the shape of the + matrix being plotted. + + - C{value_format}: a format string or a callable that specifies how + the values should be plotted. If it is a callable, it must be a + function that expects a single value and returns a string. + Example: C{"%#.2f"} for floating-point numbers with always exactly + two digits after the decimal point. See the Python documentation of + the C{%} operator for details on the format string. If the format + string is not given, it defaults to the C{str} function. + + If only the row names or the column names are given and the matrix + is square-shaped, the same names are used for both column and row + names. + """ + context = self.context + Matrix = matrix.__class__ + + bbox = self.bbox = kwds.pop("bbox", None) + palette = kwds.pop("palette", None) + if bbox is None: + raise ValueError("bbox is required for Cairo plots") + if palette is None: + raise ValueError("palette is required for Cairo plots") + + grid_width = float(kwds.get("grid_width", 1.0)) + border_width = float(kwds.get("border_width", 1.0)) + style = kwds.get("style", "boolean") + row_names = kwds.get("row_names") + col_names = kwds.get("col_names", row_names) + values = kwds.get("values") + value_format = kwds.get("value_format", str) + + # Validations + if style not in ("boolean", "palette", "none", None): + raise ValueError("invalid style") + if style == "none": + style = None + if row_names is None and col_names is not None: + row_names = col_names + if row_names is not None: + row_names = [str(name) for name in islice(row_names, matrix._nrow)] + if len(row_names) < matrix._nrow: + row_names.extend([""] * (matrix._nrow - len(row_names))) + if col_names is not None: + col_names = [str(name) for name in islice(col_names, matrix._ncol)] + if len(col_names) < matrix._ncol: + col_names.extend([""] * (matrix._ncol - len(col_names))) + if values is False: + values = None + if values is True: + values = matrix + if isinstance(values, list): + values = Matrix(list) + if values is not None and not isinstance(values, Matrix): + raise TypeError("values must be None, False, True or a matrix") + if values is not None and values.shape != matrix.shape: + raise ValueError("values must be a matrix of size %s" % matrix.shape) + + # Calculate text extents if needed + if row_names is not None or col_names is not None: + te = context.text_extents + space_width = te(" ")[4] + if row_names is not None: + max_row_name_width = max([te(s)[4] for s in row_names]) + space_width + else: + max_row_name_width = 0 + if col_names is not None: + max_col_name_width = max([te(s)[4] for s in col_names]) + space_width + else: + max_col_name_width = 0 + else: + max_row_name_width, max_col_name_width = 0, 0 + space_width = 0 + + # Calculate sizes + total_width = float(bbox.width) - max_row_name_width + total_height = float(bbox.height) - max_col_name_width + dx = total_width / matrix.shape[1] + dy = total_height / matrix.shape[0] + if kwds.get("square", True): + dx, dy = min(dx, dy), min(dx, dy) + total_width, total_height = dx * matrix.shape[1], dy * matrix.shape[0] + ox = bbox.left + (bbox.width - total_width - max_row_name_width) / 2.0 + oy = bbox.top + (bbox.height - total_height - max_col_name_width) / 2.0 + ox += max_row_name_width + oy += max_col_name_width + + # Determine rescaling factors for the palette if needed + if style == "palette": + mi, ma = matrix.min(), matrix.max() + color_offset = mi + color_ratio = (len(palette) - 1) / float(ma - mi) + else: + color_offset, color_ratio = 0, 1 + + # Validate grid width + if dx < 3 * grid_width or dy < 3 * grid_width: + grid_width = 0.0 + if grid_width > 0: + context.set_line_width(grid_width) + else: + # When the grid width is zero, we will still stroke the + # rectangles, but with the same color as the fill color + # of the cell - otherwise we would get thin white lines + # between the cells as a drawing artifact + context.set_line_width(1) + + # Draw row names (if any) + context.set_source_rgb(0.0, 0.0, 0.0) + if row_names is not None: + x, y = ox, oy + for heading in row_names: + _, _, _, h, xa, _ = context.text_extents(heading) + context.move_to(x - xa - space_width, y + (dy + h) / 2.0) + context.show_text(heading) + y += dy + + # Draw column names (if any) + if col_names is not None: + context.save() + context.translate(ox, oy) + context.rotate(-pi / 2) + x, y = 0.0, 0.0 + for heading in col_names: + _, _, _, h, _, _ = context.text_extents(heading) + context.move_to(x + space_width, y + (dx + h) / 2.0) + context.show_text(heading) + y += dx + context.restore() + + # Draw matrix + x, y = ox, oy + if style is None: + fill = lambda: None # noqa: E731 + else: + fill = context.fill_preserve + for row in matrix: + for item in row: + if item is None: + x += dx + continue + if style == "boolean": + if item: + context.set_source_rgb(0.0, 0.0, 0.0) + else: + context.set_source_rgb(1.0, 1.0, 1.0) + elif style == "palette": + cidx = int((item - color_offset) * color_ratio) + if cidx < 0: + cidx = 0 + context.set_source_rgba(*palette.get(cidx)) + context.rectangle(x, y, dx, dy) + if grid_width > 0: + fill() + context.set_source_rgb(0.5, 0.5, 0.5) + context.stroke() + else: + fill() + context.stroke() + x += dx + x, y = ox, y + dy + + # Draw cell values + if values is not None: + x, y = ox, oy + context.set_source_rgb(0.0, 0.0, 0.0) + for row in values.data: + if hasattr(value_format, "__call__"): + values = [value_format(item) for item in row] + else: + values = [value_format % item for item in row] + for item in values: + th, tw = context.text_extents(item)[3:5] + context.move_to(x + (dx - tw) / 2.0, y + (dy + th) / 2.0) + context.show_text(item) + x += dx + x, y = ox, y + dy + + # Draw borders + if border_width > 0: + context.set_line_width(border_width) + context.set_source_rgb(0.0, 0.0, 0.0) + context.rectangle(ox, oy, dx * matrix.shape[1], dy * matrix.shape[0]) + context.stroke() diff --git a/src/igraph/drawing/cairo/palette.py b/src/igraph/drawing/cairo/palette.py new file mode 100644 index 000000000..9c0e67a16 --- /dev/null +++ b/src/igraph/drawing/cairo/palette.py @@ -0,0 +1,53 @@ +"""This module provides implementation for a Cairo-specific palette drawer""" + +from igraph.drawing.cairo.base import AbstractCairoDrawer + +__all__ = ("CairoPaletteDrawer",) + + +class CairoPaletteDrawer(AbstractCairoDrawer): + """Default Cairo drawer object for palettes""" + + def __init__(self, context): + """Constructs the vertex drawer and associates it to the given + palette. + + @param context: the context on which we will draw + """ + super().__init__(context, bbox=None) + + def draw(self, palette, **kwds): + """TODO""" + from igraph.datatypes import Matrix + from igraph.drawing.utils import str_to_orientation + + context = self.context + orientation = str_to_orientation(kwds.get("orientation", "lr")) + + # Construct a matrix and plot that + indices = list(range(len(self))) + if orientation in ("rl", "bt"): + indices.reverse() + if orientation in ("lr", "rl"): + matrix = Matrix([indices]) + else: + matrix = Matrix([[i] for i in indices]) + + bbox = self.bbox = kwds.pop("bbox", None) + if bbox is None: + raise ValueError("bbox is required for Cairo plots") + + border_width = float(kwds.get("border_width", 1.0)) + grid_width = float(kwds.get("grid_width", 0.0)) + + return matrix.__plot__( + 'cairo', + context, + bbox=bbox, + palette=self, + style="palette", + square=False, + grid_width=grid_width, + border_width=border_width, + ) + diff --git a/src/igraph/drawing/cairo/plot.py b/src/igraph/drawing/cairo/plot.py new file mode 100644 index 000000000..663975378 --- /dev/null +++ b/src/igraph/drawing/cairo/plot.py @@ -0,0 +1,360 @@ +""" +Drawing and plotting routines for IGraph. + +IGraph has two plotting backends at the moment: Cairo and Matplotlib. + +The Cairo backend is dependent on the C{pycairo} or C{cairocffi} libraries that +provide Python bindings to the popular U{Cairo library}. +This means that if you don't have U{pycairo} +or U{cairocffi} installed, you won't be able +to use the Cairo plotting backend. Whenever the documentation refers to the +C{pycairo} library, you can safely replace it with C{cairocffi} as the two are +API-compatible. + +The Matplotlib backend uses the U{Matplotlib library}. +You will need to install it from PyPI if you want to use the Matplotlib +plotting backend. + +If you do not want to (or cannot) install any of the dependencies outlined +above, you can still save the graph to an SVG file and view it from +U{Mozilla Firefox} (free) or edit it in +U{Inkscape} (free), U{Skencil} +(formerly known as Sketch, also free) or Adobe Illustrator. +""" + + +import os + +from io import BytesIO +from warnings import warn + +from igraph.configuration import Configuration +from igraph.drawing.cairo.utils import find_cairo +from igraph.drawing.colors import Palette, palettes +from igraph.drawing.utils import BoundingBox +from igraph.utils import named_temporary_file + +__all__ = ("CairoPlot",) + +cairo = find_cairo() + +##################################################################### + + +class CairoPlot: + """Class representing an arbitrary plot that uses the Cairo plotting + backend. + + Objects that you can plot include graphs, matrices, palettes, clusterings, + covers, and dendrograms. + + In Cairo, every plot has an associated surface object. The surface is an + instance of C{cairo.Surface}, a member of the C{pycairo} library. The + surface itself provides a unified API to various plotting targets like SVG + files, X11 windows, PostScript files, PNG files and so on. C{igraph} does + not usually know on which surface it is plotting at each point in time, + since C{pycairo} takes care of the actual drawing. Everything that's + supported by C{pycairo} should be supported by this class as well. + + Current Cairo surfaces include: + + - C{cairo.GlitzSurface} -- OpenGL accelerated surface for the X11 + Window System. + + - C{cairo.ImageSurface} -- memory buffer surface. Can be written to a + C{PNG} image file. + + - C{cairo.PDFSurface} -- PDF document surface. + + - C{cairo.PSSurface} -- PostScript document surface. + + - C{cairo.SVGSurface} -- SVG (Scalable Vector Graphics) document surface. + + - C{cairo.Win32Surface} -- Microsoft Windows screen rendering. + + - C{cairo.XlibSurface} -- X11 Window System screen rendering. + + If you create a C{Plot} object with a string given as the target surface, + the string will be treated as a filename, and its extension will decide + which surface class will be used. Please note that not all surfaces might + be available, depending on your C{pycairo} installation. + + A C{Plot} has an assigned default palette (see L{igraph.drawing.colors.Palette}) + which is used for plotting objects. + + A C{Plot} object also has a list of objects to be plotted with their + respective bounding boxes, palettes and opacities. Palettes assigned to an + object override the default palette of the plot. Objects can be added by the + L{Plot.add} method and removed by the L{Plot.remove} method. + """ + + def __init__( + self, + target=None, + bbox=None, + palette=None, + background=None, + ): + """Creates a new plot. + + @param target: the target surface to write to. It can be one of the + following types: + + - C{None} -- a Cairo surface will be created and the object will be + plotted there. + + - C{cairo.Surface} -- the given Cairo surface will be used. + + - C{string} -- a file with the given name will be created and an + appropriate Cairo surface will be attached to it. + + @param bbox: the bounding box of the surface. It is interpreted + differently with different surfaces: PDF and PS surfaces will treat it + as points (1 point = 1/72 inch). Image surfaces will treat it as + pixels. SVG surfaces will treat it as an abstract unit, but it will + mostly be interpreted as pixels when viewing the SVG file in Firefox. + + @param palette: the palette primarily used on the plot if the + added objects do not specify a private palette. Must be either + an L{igraph.drawing.colors.Palette} object or a string referring + to a valid key of C{igraph.drawing.colors.palettes} (see module + L{igraph.drawing.colors}) or C{None}. In the latter case, the default + palette given by the configuration key C{plotting.palette} is used. + + @param background: the background color. If C{None}, the background + will be transparent. You can use any color specification here that is + understood by L{igraph.drawing.colors.color_name_to_rgba}. + """ + + self._filename = None + self._need_tmpfile = False + + if bbox is None: + self.bbox = BoundingBox(600, 600) + elif isinstance(bbox, tuple) or isinstance(bbox, list): + self.bbox = BoundingBox(bbox) + else: + self.bbox = bbox + + if palette is None: + config = Configuration.instance() + palette = config["plotting.palette"] + if not isinstance(palette, Palette): + palette = palettes[palette] + self._palette = palette + + if target is None: + self._need_tmpfile = True + self._surface = cairo.ImageSurface( + cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) + ) + elif isinstance(target, cairo.Surface): + self._surface = target + else: + self._filename = target + _, ext = os.path.splitext(target) + ext = ext.lower() + if ext == ".pdf": + self._surface = cairo.PDFSurface( + target, self.bbox.width, self.bbox.height + ) + elif ext == ".ps" or ext == ".eps": + self._surface = cairo.PSSurface( + target, self.bbox.width, self.bbox.height + ) + elif ext == ".png": + self._surface = cairo.ImageSurface( + cairo.FORMAT_ARGB32, int(self.bbox.width), int(self.bbox.height) + ) + elif ext == ".svg": + self._surface = cairo.SVGSurface( + target, self.bbox.width, self.bbox.height + ) + else: + raise ValueError("image format not handled by Cairo: %s" % ext) + + self._ctx = cairo.Context(self._surface) + + self._objects = [] + self._is_dirty = False + + if background is None: + background = "white" + self.background = background + + def add(self, obj, bbox=None, palette=None, opacity=1.0, *args, **kwds): + """Adds an object to the plot. + + Arguments not specified here are stored and passed to the object's + plotting function when necessary. Since you are most likely interested + in the arguments acceptable by graphs, see L{Graph.__plot__} for more + details. + + @param obj: the object to be added + @param bbox: the bounding box of the object. If C{None}, the object + will fill the entire area of the plot. + @param palette: the color palette used for drawing the object. If the + object tries to get a color assigned to a positive integer, it + will use this palette. If C{None}, defaults to the global palette + of the plot. + @param opacity: the opacity of the object being plotted, in the range + 0.0-1.0 + + @see: Graph.__plot__ + """ + if opacity < 0.0 or opacity > 1.0: + raise ValueError("opacity must be between 0.0 and 1.0") + if bbox is None: + bbox = self.bbox + if (bbox is not None) and (not isinstance(bbox, BoundingBox)): + bbox = BoundingBox(bbox) + self._objects.append((obj, bbox, palette, opacity, args, kwds)) + self.mark_dirty() + + @property + def background(self): + """Returns the background color of the plot. C{None} means a + transparent background. + """ + return self._background + + @background.setter + def background(self, color): + """Sets the background color of the plot. C{None} means a + transparent background. You can use any color specification here + that is understood by the C{get} method of the current palette + or by L{igraph.drawing.colors.color_name_to_rgb}. + """ + if color is None: + self._background = None + else: + self._background = self._palette.get(color) + + def remove(self, obj, bbox=None, idx=1): + """Removes an object from the plot. + + If the object has been added multiple times and no bounding box + was specified, it removes the instance which occurs M{idx}th + in the list of identical instances of the object. + + @param obj: the object to be removed + @param bbox: optional bounding box specification for the object. + If given, only objects with exactly this bounding box will be + considered. + @param idx: if multiple objects match the specification given by + M{obj} and M{bbox}, only the M{idx}th occurrence will be removed. + @return: C{True} if the object has been removed successfully, + C{False} if the object was not on the plot at all or M{idx} + was larger than the count of occurrences + """ + for i in range(len(self._objects)): + current_obj, current_bbox = self._objects[i][0:2] + if current_obj is obj and (bbox is None or current_bbox == bbox): + idx -= 1 + if idx == 0: + self._objects[i : (i + 1)] = [] + self.mark_dirty() + return True + return False + + def mark_dirty(self): + """Marks the plot as dirty (should be redrawn)""" + self._is_dirty = True + + def redraw(self, context=None): + """Redraws the plot""" + ctx = context or self._ctx + if self._background is not None: + ctx.set_source_rgba(*self._background) + ctx.rectangle(0, 0, self.bbox.width, self.bbox.height) + ctx.fill() + + for obj, bbox, palette, opacity, args, kwds in self._objects: + if palette is None: + palette = getattr(obj, "_default_palette", self._palette) + plotter = getattr(obj, "__plot__", None) + if plotter is None: + warn("%s does not support plotting" % (obj,)) + else: + if opacity < 1.0: + ctx.push_group() + else: + ctx.save() + plotter("cairo", ctx, bbox=bbox, palette=palette, *args, **kwds) + if opacity < 1.0: + ctx.pop_group_to_source() + ctx.paint_with_alpha(opacity) + else: + ctx.restore() + + self._is_dirty = False + + def save(self, fname=None): + """Saves the plot. + + @param fname: the filename to save to. It is ignored if the surface + of the plot is not an C{ImageSurface}. + """ + if self._is_dirty: + self.redraw() + if isinstance(self._surface, cairo.ImageSurface): + if fname is None and self._need_tmpfile: + with named_temporary_file(prefix="igraph", suffix=".png") as fname: + self._surface.write_to_png(fname) + return None + + fname = fname or self._filename + if fname is None: + raise ValueError("no file name is known for the surface and none given") + + # Conversion to string is needed because the user might pass a Path + # object and cairocffi expects a string + return self._surface.write_to_png(str(fname)) + + if fname is not None: + warn("filename is ignored for surfaces other than ImageSurface") + + self._ctx.show_page() + self._surface.finish() + + def _repr_svg_(self): + """Returns an SVG representation of this plot as a string. + + This method is used by IPython to display this plot inline. + """ + io = BytesIO() + # Create a new SVG surface and use that to get the SVG representation, + # which will end up in io + surface = cairo.SVGSurface(io, self.bbox.width, self.bbox.height) + context = cairo.Context(surface) + # Plot the graph on this context + self.redraw(context) + # No idea why this is needed but python crashes without + context.show_page() + surface.finish() + # Return the raw SVG representation + result = io.getvalue().decode("utf-8") + return result, {"isolated": True} # put it inside an iframe + + @property + def bounding_box(self): + """Returns the bounding box of the Cairo surface as a + L{BoundingBox} object""" + return BoundingBox(self.bbox) + + @property + def height(self): + """Returns the height of the Cairo surface on which the plot + is drawn""" + return self.bbox.height + + @property + def surface(self): + """Returns the Cairo surface on which the plot is drawn""" + return self._surface + + @property + def width(self): + """Returns the width of the Cairo surface on which the plot + is drawn""" + return self.bbox.width diff --git a/src/igraph/drawing/cairo/polygon.py b/src/igraph/drawing/cairo/polygon.py new file mode 100644 index 000000000..8b350d6d5 --- /dev/null +++ b/src/igraph/drawing/cairo/polygon.py @@ -0,0 +1,86 @@ +from igraph.drawing.utils import calculate_corner_radii +from igraph.utils import consecutive_pairs + +from .base import AbstractCairoDrawer + +__all__ = ("CairoPolygonDrawer",) + + +class CairoPolygonDrawer(AbstractCairoDrawer): + """Class that is used to draw polygons in Cairo. + + The corner points of the polygon can be set by the C{points} + property of the drawer, or passed at construction time. Most + drawing methods in this class also have an extra C{points} + argument that can be used to override the set of points in the + C{points} property.""" + + def __init__(self, context, bbox=(1, 1)): + """Constructs a new polygon drawer that draws on the given + Cairo context. + + @param context: the Cairo context to draw on + @param bbox: ignored, leave it at its default value + @param points: the list of corner points + """ + super().__init__(context, bbox) + + def draw_path(self, points, corner_radius=0): + """Sets up a Cairo path for the outline of a polygon on the given + Cairo context. + + @param points: the coordinates of the corners of the polygon, + in clockwise or counter-clockwise order. + @param corner_radius: if zero, an ordinary polygon will be drawn. + If positive, the corners of the polygon will be rounded with + the given radius. + """ + self.context.new_path() + + if len(points) < 2: + # Well, a polygon must have at least two corner points + return + + ctx = self.context + if corner_radius <= 0: + # No rounded corners, this is simple + ctx.move_to(*points[-1]) + for point in points: + ctx.line_to(*point) + return + + # Rounded corners. First, we will take each side of the + # polygon and find what the corner radius should be on + # each corner. If the side is longer than 2r (where r is + # equal to corner_radius), the radius allowed by that side + # is r; if the side is shorter, the radius is the length + # of the side / 2. For each corner, the final corner radius + # is the smaller of the radii on the two sides adjacent to + # the corner. + corner_radii = calculate_corner_radii(points, corner_radius) + + # Okay, move to the last corner, adjusted by corner_radii[-1] + # towards the first corner + ctx.move_to(*(points[-1].towards(points[0], corner_radii[-1]))) + # Now, for each point in points, draw a line towards the + # corner, stopping before it in a distance of corner_radii[idx], + # then draw the corner + u = points[-1] + for idx, (v, w) in enumerate(consecutive_pairs(points, True)): + radius = corner_radii[idx] + ctx.line_to(*v.towards(u, radius)) + aux1 = v.towards(u, radius / 2) + aux2 = v.towards(w, radius / 2) + ctx.curve_to( + aux1.x, aux1.y, aux2.x, aux2.y, *v.towards(w, corner_radii[idx]) + ) + u = v + + def draw(self, points): + """Draws the polygon using the current stroke of the Cairo context. + + @param points: the coordinates of the corners of the polygon, + in clockwise or counter-clockwise order. + """ + self.draw_path(points) + self.context.stroke() diff --git a/src/igraph/drawing/cairo/text.py b/src/igraph/drawing/cairo/text.py new file mode 100644 index 000000000..5e922996d --- /dev/null +++ b/src/igraph/drawing/cairo/text.py @@ -0,0 +1,358 @@ +""" +Drawers for labels on plots. +""" + +import re +from warnings import warn + +from igraph.drawing.cairo.base import AbstractCairoDrawer + +__all__ = ("CairoTextDrawer", ) + + +class CairoTextDrawer(AbstractCairoDrawer): + """Class that draws text on a Cairo context. + + This class supports multi-line text unlike the original Cairo text + drawing methods.""" + + LEFT, CENTER, RIGHT = "left", "center", "right" + TOP, BOTTOM = "top", "bottom" + + def __init__(self, context, text="", halign="center", valign="center"): + """Constructs a new instance that will draw the given `text` on + the given Cairo `context`.""" + super().__init__(context, (0, 0)) + self.text = text + self.halign = halign + self.valign = valign + + def draw(self, wrap=False): + """Draws the text in the current bounding box of the drawer. + + Since the class itself is an instance of `AbstractCairoDrawer`, it + has an attribute named ``bbox`` which will be used as a bounding + box. + + @param wrap: whether to allow re-wrapping of the text if it does not + fit within the bounding box horizontally. + """ + ctx = self.context + bbox = self.bbox + + text_layout = self.get_text_layout(bbox.left, bbox.top, bbox.width, wrap) + if not text_layout: + return + + _, font_descent, line_height = ctx.font_extents()[:3] + yb = ctx.text_extents(text_layout[0][2])[1] + total_height = len(text_layout) * line_height + + if self.valign == self.BOTTOM: + # Bottom vertical alignment + dy = bbox.height - total_height - yb + font_descent + elif self.valign == self.CENTER: + # Centered vertical alignment + dy = (bbox.height - total_height - yb + font_descent + line_height) / 2.0 + else: + # Top vertical alignment + dy = line_height + + for ref_x, ref_y, line in text_layout: + ctx.move_to(ref_x, ref_y + dy) + ctx.show_text(line) + ctx.new_path() + + def get_text_layout(self, x=None, y=None, width=None, wrap=False): + """Calculates the layout of the current text. `x` and `y` denote the + coordinates where the drawing should start. If they are both ``None``, + the current position of the context will be used. + + Vertical alignment settings are not taken into account in this method + as the text is not drawn within a box. + + @param x: The X coordinate of the reference point where the layout should + start. + @param y: The Y coordinate of the reference point where the layout should + start. + @param width: The width of the box in which the text will be fitted. It + matters only when the text is right-aligned or centered. The text + will overflow the box if any of the lines is longer than the box + width and `wrap` is ``False``. + @param wrap: whether to allow re-wrapping of the text if it does not + fit within the given width. + + @return: a list consisting of ``(x, y, line)`` tuples where ``x`` and + ``y`` refer to reference points on the Cairo canvas and ``line`` + refers to the corresponding text that should be plotted there. + """ + ctx = self.context + + if x is None or y is None: + x, y = ctx.get_current_point() + + line_height = ctx.font_extents()[2] + + if wrap: + if width and width > 0: + iterlines = self._iterlines_wrapped(width) + else: + warn("ignoring wrap=True as no width was specified") + else: + iterlines = self._iterlines() + + result = [] + + if self.halign == self.CENTER: + # Centered alignment + if width is None: + width = self.text_extents()[2] + for line, line_width, x_bearing in iterlines: + result.append((x + (width - line_width) / 2.0 - x_bearing, y, line)) + y += line_height + + elif self.halign == self.RIGHT: + # Right alignment + if width is None: + width = self.text_extents()[2] + x += width + for line, line_width, x_bearing in iterlines: + result.append((x - line_width - x_bearing, y, line)) + y += line_height + + else: + # Left alignment + for line, _, x_bearing in iterlines: + result.append((x - x_bearing, y, line)) + y += line_height + + return result + + def draw_at(self, x=None, y=None, width=None, wrap=False): + """Draws the text by setting up an appropriate path on the Cairo + context and filling it. `x` and `y` denote the coordinates where the + drawing should start. If they are both ``None``, the current position + of the context will be used. + + Vertical alignment settings are not taken into account in this method + as the text is not drawn within a box. + + @param x: The X coordinate of the reference point where the layout should + start. + @param y: The Y coordinate of the reference point where the layout should + start. + @param width: The width of the box in which the text will be fitted. It + matters only when the text is right-aligned or centered. The text + will overflow the box if any of the lines is longer than the box + width and `wrap` is ``False``. + @param wrap: whether to allow re-wrapping of the text if it does not + fit within the given width. + """ + ctx = self.context + for ref_x, ref_y, line in self.get_text_layout(x, y, width, wrap): + ctx.move_to(ref_x, ref_y) + ctx.show_text(line) + ctx.new_path() + + def _iterlines(self): + """Iterates over the label line by line and returns a tuple containing + the folloing for each line: the line itself, the width of the line and + the X-bearing of the line.""" + ctx = self.context + for line in self._text.split("\n"): + xb, _, line_width, _, _, _ = ctx.text_extents(line) + yield (line, line_width, xb) + + def _iterlines_wrapped(self, width): + """Iterates over the label line by line and returns a tuple containing + the folloing for each line: the line itself, the width of the line and + the X-bearing of the line. + + The difference between this method and `_iterlines()` is that this + method is allowed to re-wrap the line if necessary. + + @param width: The width of the box in which the text will be fitted. + Lines will be wrapped if they are wider than this width. + """ + ctx = self.context + for line in self._text.split("\n"): + xb, _, line_width, _, _, _ = ctx.text_extents(line) + if line_width <= width: + yield (line, line_width, xb) + continue + + # We have to wrap the line + current_line, current_width, last_sep_width = [], 0, 0 + for match in re.finditer(r"(\S+)(\s+)?", line): + word, sep = match.groups() + word_width = ctx.text_extents(word)[4] + if sep: + sep_width = ctx.text_extents(sep)[4] + else: + sep_width = 0 + current_width += word_width + if current_width >= width and current_line: + yield ("".join(current_line), current_width - word_width, 0) + # Starting a new line + current_line, current_width = [word], word_width + if sep is not None: + current_line.append(sep) + else: + current_width += last_sep_width + current_line.append(word) + if sep is not None: + current_line.append(sep) + last_sep_width = sep_width + if current_line: + yield ("".join(current_line), current_width, 0) + + @property + def text(self): + """Returns the text to be drawn.""" + return self._text + + @text.setter + def text(self, text): + """Sets the text that will be drawn. + + If `text` is ``None``, it will be mapped to an empty string; otherwise, + it will be converted to a string.""" + if text is None: + self._text = "" + else: + self._text = str(text) + + def text_extents(self): + """Returns the X-bearing, Y-bearing, width, height, X-advance and + Y-advance of the text. + + For multi-line text, the X-bearing and Y-bearing correspond to the + first line, while the X-advance is extracted from the last line. + and the Y-advance is the sum of all the Y-advances. The width and + height correspond to the entire bounding box of the text.""" + lines = self.text.split("\n") + if len(lines) <= 1: + return self.context.text_extents(self.text) + + ( + x_bearing, + y_bearing, + width, + height, + x_advance, + y_advance, + ) = self.context.text_extents(lines[0]) + + line_height = self.context.font_extents()[2] + for line in lines[1:]: + _, _, w, _, x_advance, ya = self.context.text_extents(line) + width = max(width, w) + height += line_height + y_advance += ya + + return x_bearing, y_bearing, width, height, x_advance, y_advance + + +def test(): + """Testing routine for L{CairoTextDrawer}""" + import math + from igraph.drawing.cairo.utils import find_cairo + from igraph.drawing.text import TextAlignment + + cairo = find_cairo() + + text = "The quick brown fox\njumps over a\nlazy dog" + width, height = (600, 1000) + + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + context = cairo.Context(surface) + drawer = CairoTextDrawer(context, text) + + context.set_source_rgb(1, 1, 1) + context.set_font_size(16.0) + context.rectangle(0, 0, width, height) + context.fill() + + context.set_source_rgb(0.5, 0.5, 0.5) + for i in range(200, width, 200): + context.move_to(i, 0) + context.line_to(i, height) + context.stroke() + for i in range(200, height, 200): + context.move_to(0, i) + context.line_to(width, i) + context.stroke() + context.set_source_rgb(0.75, 0.75, 0.75) + context.set_line_width(0.5) + for i in range(100, width, 200): + context.move_to(i, 0) + context.line_to(i, height) + context.stroke() + for i in range(100, height, 200): + context.move_to(0, i) + context.line_to(width, i) + context.stroke() + + def mark_point(red, green, blue): + """Marks the current point on the canvas by the given color""" + x, y = context.get_current_point() + context.set_source_rgba(red, green, blue, 0.5) + context.arc(x, y, 4, 0, 2 * math.pi) + context.fill() + + # Testing drawer.draw_at() + alignments = TextAlignment.LEFT, TextAlignment.CENTER, TextAlignment.RIGHT + for i, halign in enumerate(alignments): + # Mark the reference points + context.move_to(i * 200, 40) + mark_point(0, 0, 1) + context.move_to(i * 200, 140) + mark_point(0, 0, 1) + + # Draw the text + context.set_source_rgb(0, 0, 0) + drawer.halign = halign + drawer.draw_at(i * 200, 40) + drawer.draw_at(i * 200, 140, width=200) + + # Mark the new reference point + mark_point(1, 0, 0) + + # Testing TextDrawer.draw() + for i, halign in enumerate(("left", "center", "right")): + for j, valign in enumerate(("top", "center", "bottom")): + # Draw the text + context.set_source_rgb(0, 0, 0) + drawer.halign = halign + drawer.valign = valign + drawer.bbox = (i * 200, j * 200 + 200, i * 200 + 200, j * 200 + 400) + drawer.draw() + # Mark the new reference point + mark_point(1, 0, 0) + + # Testing TextDrawer.wrap() + drawer.text = ( + "Jackdaws love my big sphinx of quartz. Yay, wrapping! " + + "Jackdaws love my big sphinx of quartz.\n\n" + + "Jackdaws love my big sphinx of quartz." + ) + drawer.valign = TextAlignment.TOP + for i, halign in enumerate(("left", "center", "right")): + context.move_to(i * 200, 840) + + # Mark the reference point + mark_point(0, 0, 1) + + # Draw the text + context.set_source_rgb(0, 0, 0) + drawer.halign = halign + drawer.draw_at(i * 200, 840, width=199, wrap=True) + + # Mark the new reference point + mark_point(1, 0, 0) + + surface.write_to_png("test.png") + + +if __name__ == "__main__": + test() diff --git a/src/igraph/drawing/cairo/utils.py b/src/igraph/drawing/cairo/utils.py new file mode 100644 index 000000000..753652b83 --- /dev/null +++ b/src/igraph/drawing/cairo/utils.py @@ -0,0 +1,20 @@ +from igraph.drawing.utils import FakeModule +from typing import Any + +__all__ = ("find_cairo", ) + + +def find_cairo() -> Any: + """Tries to import the ``cairo`` Python module if it is installed, + also trying ``cairocffi`` (a drop-in replacement of ``cairo``). + Returns a fake module if everything fails. + """ + module_names = ["cairo", "cairocffi"] + module = FakeModule("Plotting not available; please install pycairo or cairocffi") + for module_name in module_names: + try: + module = __import__(module_name) + break + except ImportError: + pass + return module diff --git a/src/igraph/drawing/vertex.py b/src/igraph/drawing/cairo/vertex.py similarity index 63% rename from src/igraph/drawing/vertex.py rename to src/igraph/drawing/cairo/vertex.py index fbae32e3f..8d5fdf8ab 100644 --- a/src/igraph/drawing/vertex.py +++ b/src/igraph/drawing/cairo/vertex.py @@ -1,44 +1,16 @@ +"""This module provides implementations of Cairo-specific vertex drawers, i.e. +drawers that the Cairo graph drawer will use to draw vertices. """ -Drawing routines to draw the vertices of graphs. -This module provides implementations of vertex drawers, i.e. drawers that the -default graph drawer will use to draw vertices. -""" +from math import pi -from igraph.drawing.baseclasses import AbstractDrawer, AbstractCairoDrawer +from igraph.drawing.baseclasses import AbstractVertexDrawer from igraph.drawing.metamagic import AttributeCollectorBase from igraph.drawing.shapes import ShapeDrawerDirectory -from math import pi - -__all__ = ("AbstractVertexDrawer", "AbstractCairoVertexDrawer", "DefaultVertexDrawer") - - -class AbstractVertexDrawer(AbstractDrawer): - """Abstract vertex drawer object from which all concrete vertex drawer - implementations are derived.""" - - def __init__(self, palette, layout): - """Constructs the vertex drawer and associates it to the given - palette. - @param palette: the palette that can be used to map integer - color indices to colors when drawing vertices - @param layout: the layout of the vertices in the graph being drawn - """ - self.layout = layout - self.palette = palette +from .base import AbstractCairoDrawer - def draw(self, visual_vertex, vertex, coords): - """Draws the given vertex. - - @param visual_vertex: object specifying the visual properties of the - vertex. Its structure is defined by the VisualVertexBuilder of the - L{DefaultGraphDrawer}; see its source code. - @param vertex: the raw igraph vertex being drawn - @param coords: the X and Y coordinates of the vertex as specified by the - layout algorithm, scaled into the bounding box. - """ - raise NotImplementedError("abstract class") +__all__ = ("AbstractCairoVertexDrawer", "CairoVertexDrawer") class AbstractCairoVertexDrawer(AbstractVertexDrawer, AbstractCairoDrawer): @@ -61,11 +33,11 @@ def __init__(self, context, bbox, palette, layout): AbstractVertexDrawer.__init__(self, palette, layout) -class DefaultVertexDrawer(AbstractCairoVertexDrawer): +class CairoVertexDrawer(AbstractCairoVertexDrawer): """The default vertex drawer implementation of igraph.""" def __init__(self, context, bbox, palette, layout): - AbstractCairoVertexDrawer.__init__(self, context, bbox, palette, layout) + super().__init__(context, bbox, palette, layout) self.VisualVertexBuilder = self._construct_visual_vertex_builder() def _construct_visual_vertex_builder(self): diff --git a/src/igraph/drawing/colors.py b/src/igraph/drawing/colors.py index 78ecc275f..72813dc4b 100644 --- a/src/igraph/drawing/colors.py +++ b/src/igraph/drawing/colors.py @@ -4,8 +4,7 @@ Color handling functions. """ -from igraph.datatypes import Matrix -from igraph.utils import str_to_orientation +from abc import ABCMeta, abstractmethod from math import ceil __all__ = ( @@ -26,11 +25,12 @@ "rgb_to_hsl", "rgba_to_hsla", "palettes", + "default_edge_colors", "known_colors", ) -class Palette: +class Palette(metaclass=ABCMeta): """Base class of color palettes. Color palettes are mappings that assign integers from the range @@ -117,6 +117,7 @@ class will simply try to interpret it as a single color by # Multiple colors return [self.get(color) for color in colors] + @abstractmethod def _get(self, v): """Override this method in a subclass to create a custom palette. @@ -125,7 +126,7 @@ def _get(self, v): @param v: numerical index of the color to be retrieved @return: a 4-tuple containing the RGBA values""" - raise NotImplementedError("abstract class") + raise NotImplementedError __getitem__ = get @@ -138,10 +139,19 @@ def __len__(self): """Returns the number of colors in this palette""" return self._length - def __plot__(self, context, bbox, palette, *args, **kwds): - """Plots the colors of the palette on the given Cairo context + def __plot__(self, backend, context, *args, **kwds): + """Plots the colors of the palette on the given Cairo context/mpl Axes - Supported keyword arguments are: + Supported keywork arguments in both Cairo and matplotlib are: + + - C{orientation}: the orientation of the palette. Must be one of + the following values: C{left-right}, C{bottom-top}, C{right-left} + or C{top-bottom}. Possible aliases: C{horizontal} = C{left-right}, + C{vertical} = C{bottom-top}, C{lr} = C{left-right}, + C{rl} = C{right-left}, C{tb} = C{top-bottom}, C{bt} = C{bottom-top}. + The default is C{left-right}. + + Additional supported keyword arguments in Cairo are: - C{border_width}: line width of the border shown around the palette. If zero or negative, the border is turned off. Default is C{1}. @@ -151,35 +161,12 @@ def __plot__(self, context, bbox, palette, *args, **kwds): turned off if the size of a cell is less than three times the given line width. Default is C{0}. Fractional widths are also allowed. - - C{orientation}: the orientation of the palette. Must be one of - the following values: C{left-right}, C{bottom-top}, C{right-left} - or C{top-bottom}. Possible aliases: C{horizontal} = C{left-right}, - C{vertical} = C{bottom-top}, C{lr} = C{left-right}, - C{rl} = C{right-left}, C{tb} = C{top-bottom}, C{bt} = C{bottom-top}. - The default is C{left-right}. + Keyword arguments in matplotlib are passes to Axes.imshow. """ - border_width = float(kwds.get("border_width", 1.0)) - grid_width = float(kwds.get("grid_width", 0.0)) - orientation = str_to_orientation(kwds.get("orientation", "lr")) - - # Construct a matrix and plot that - indices = list(range(len(self))) - if orientation in ("rl", "bt"): - indices.reverse() - if orientation in ("lr", "rl"): - matrix = Matrix([indices]) - else: - matrix = Matrix([[i] for i in indices]) - - return matrix.__plot__( - context, - bbox, - self, - style="palette", - square=False, - grid_width=grid_width, - border_width=border_width, - ) + from igraph.drawing import DrawerDirectory + + drawer = DrawerDirectory.resolve(self, backend)(context) + drawer.draw(self, **kwds) def __repr__(self): return "<%s with %d colors>" % (self.__class__.__name__, self._length) @@ -728,6 +715,13 @@ def lighten(color, ratio=0.5): ) +default_edge_colors = { + "cairo": ["grey20", "grey80"], + "matplotlib": ["dimgrey", "silver"], + "plotly": ["rgb(51,51,51)", "rgb(204,204,204)"], +} + + known_colors = { "alice blue": (0.94117647058823528, 0.97254901960784312, 1.0, 1.0), "aliceblue": (0.94117647058823528, 0.97254901960784312, 1.0, 1.0), diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index c47b19193..cdca21f0d 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -13,628 +13,11 @@ network from Cytoscape and convert it to igraph format. """ -from math import atan2, cos, pi, sin, tan, sqrt from warnings import warn -from igraph._igraph import convex_hull, VertexSeq -from igraph.configuration import Configuration -from igraph.drawing.baseclasses import ( - AbstractDrawer, - AbstractCairoDrawer, - AbstractXMLRPCDrawer, -) -from igraph.drawing.colors import color_to_html_format, color_name_to_rgb -from igraph.drawing.edge import ArrowEdgeDrawer -from igraph.drawing.text import TextAlignment, TextDrawer -from igraph.drawing.metamagic import AttributeCollectorBase -from igraph.drawing.shapes import PolygonDrawer -from igraph.drawing.utils import find_cairo, Point -from igraph.drawing.vertex import DefaultVertexDrawer -from igraph.layout import Layout +from igraph.drawing.baseclasses import AbstractGraphDrawer, AbstractXMLRPCDrawer -__all__ = ( - "DefaultGraphDrawer", "MatplotlibGraphDrawer", "CytoscapeGraphDrawer", - "UbiGraphDrawer" -) - -cairo = find_cairo() - -##################################################################### - - -class AbstractGraphDrawer(AbstractDrawer): - """Abstract class that serves as a base class for anything that - draws an igraph.Graph.""" - - def draw(self, graph, *args, **kwds): - """Abstract method, must be implemented in derived classes.""" - raise NotImplementedError("abstract class") - - def ensure_layout(self, layout, graph=None): - """Helper method that ensures that I{layout} is an instance - of L{Layout}. If it is not, the method will try to convert - it to a L{Layout} according to the following rules: - - - If I{layout} is a string, it is assumed to be a name - of an igraph layout, and it will be passed on to the - C{layout} method of the given I{graph} if I{graph} is - not C{None}. - - - If I{layout} is C{None}, the C{layout} method of - I{graph} will be invoked with no parameters, which - will call the default layout algorithm. - - - Otherwise, I{layout} will be passed on to the constructor - of L{Layout}. This handles lists of lists, lists of tuples - and such. - - If I{layout} is already a L{Layout} instance, it will still - be copied and a copy will be returned. This is because graph - drawers are allowed to transform the layout for their purposes, - and we don't want the transformation to propagate back to the - caller. - """ - if isinstance(layout, Layout): - layout = Layout(layout.coords) - elif isinstance(layout, str) or layout is None: - layout = graph.layout(layout) - else: - layout = Layout(layout) - return layout - - -##################################################################### - - -class AbstractCairoGraphDrawer(AbstractGraphDrawer, AbstractCairoDrawer): - """Abstract base class for graph drawers that draw on a Cairo canvas.""" - - def __init__(self, context, bbox): - """Constructs the graph drawer and associates it to the given - Cairo context and the given L{BoundingBox}. - - @param context: the context on which we will draw - @param bbox: the bounding box within which we will draw. - Can be anything accepted by the constructor - of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple - or a L{BoundingBox} object). - """ - AbstractCairoDrawer.__init__(self, context, bbox) - AbstractGraphDrawer.__init__(self) - - -##################################################################### - - -class DefaultGraphDrawer(AbstractCairoGraphDrawer): - """Class implementing the default visualisation of a graph. - - The default visualisation of a graph draws the nodes on a 2D plane - according to a given L{Layout}, then draws a straight or curved - edge between nodes connected by edges. This is the visualisation - used when one invokes the L{plot()} function on a L{Graph} object. - - See L{Graph.__plot__()} for the keyword arguments understood by - this drawer.""" - - def __init__( - self, - context, - bbox, - vertex_drawer_factory=DefaultVertexDrawer, - edge_drawer_factory=ArrowEdgeDrawer, - label_drawer_factory=TextDrawer, - ): - """Constructs the graph drawer and associates it to the given - Cairo context and the given L{BoundingBox}. - - @param context: the context on which we will draw - @param bbox: the bounding box within which we will draw. - Can be anything accepted by the constructor - of L{BoundingBox} (i.e., a 2-tuple, a 4-tuple - or a L{BoundingBox} object). - @param vertex_drawer_factory: a factory method that returns an - L{AbstractCairoVertexDrawer} instance bound to a - given Cairo context. The factory method must take - three parameters: the Cairo context, the bounding - box of the drawing area and the palette to be - used for drawing colored vertices. The default - vertex drawer is L{DefaultVertexDrawer}. - @param edge_drawer_factory: a factory method that returns an - L{AbstractEdgeDrawer} instance bound to a - given Cairo context. The factory method must take - two parameters: the Cairo context and the palette - to be used for drawing colored edges. You can use - any of the actual L{AbstractEdgeDrawer} - implementations here to control the style of - edges drawn by igraph. The default edge drawer is - L{ArrowEdgeDrawer}. - @param label_drawer_factory: a factory method that returns a - L{TextDrawer} instance bound to a given Cairo - context. The method must take one parameter: the - Cairo context. The default label drawer is - L{TextDrawer}. - """ - AbstractCairoGraphDrawer.__init__(self, context, bbox) - self.vertex_drawer_factory = vertex_drawer_factory - self.edge_drawer_factory = edge_drawer_factory - self.label_drawer_factory = label_drawer_factory - - def _determine_edge_order(self, graph, kwds): - """Returns the order in which the edge of the given graph have to be - drawn, assuming that the relevant keyword arguments (C{edge_order} and - C{edge_order_by}) are given in C{kwds} as a dictionary. If neither - C{edge_order} nor C{edge_order_by} is present in C{kwds}, this - function returns C{None} to indicate that the graph drawer is free to - choose the most convenient edge ordering.""" - if "edge_order" in kwds: - # Edge order specified explicitly - return kwds["edge_order"] - - if kwds.get("edge_order_by") is None: - # No edge order specified - return None - - # Order edges by the value of some attribute - edge_order_by = kwds["edge_order_by"] - reverse = False - if isinstance(edge_order_by, tuple): - edge_order_by, reverse = edge_order_by - if isinstance(reverse, str): - reverse = reverse.lower().startswith("desc") - attrs = graph.es[edge_order_by] - edge_order = sorted( - list(range(len(attrs))), key=attrs.__getitem__, reverse=bool(reverse) - ) - - return edge_order - - def _determine_vertex_order(self, graph, kwds): - """Returns the order in which the vertices of the given graph have to be - drawn, assuming that the relevant keyword arguments (C{vertex_order} and - C{vertex_order_by}) are given in C{kwds} as a dictionary. If neither - C{vertex_order} nor C{vertex_order_by} is present in C{kwds}, this - function returns C{None} to indicate that the graph drawer is free to - choose the most convenient vertex ordering.""" - if "vertex_order" in kwds: - # Vertex order specified explicitly - return kwds["vertex_order"] - - if kwds.get("vertex_order_by") is None: - # No vertex order specified - return None - - # Order vertices by the value of some attribute - vertex_order_by = kwds["vertex_order_by"] - reverse = False - if isinstance(vertex_order_by, tuple): - vertex_order_by, reverse = vertex_order_by - if isinstance(reverse, str): - reverse = reverse.lower().startswith("desc") - attrs = graph.vs[vertex_order_by] - vertex_order = sorted( - list(range(len(attrs))), key=attrs.__getitem__, reverse=bool(reverse) - ) - - return vertex_order - - def draw(self, graph, palette, *args, **kwds): - # Some abbreviations for sake of simplicity - directed = graph.is_directed() - context = self.context - - # Calculate/get the layout of the graph - layout = self.ensure_layout(kwds.get("layout", None), graph) - - # Determine the size of the margin on each side - margin = kwds.get("margin", 0) - try: - margin = list(margin) - except TypeError: - margin = [margin] - while len(margin) < 4: - margin.extend(margin) - - # Contract the drawing area by the margin and fit the layout - bbox = self.bbox.contract(margin) - layout.fit_into(bbox, keep_aspect_ratio=kwds.get("keep_aspect_ratio", False)) - - # Decide whether we need to calculate the curvature of edges - # automatically -- and calculate them if needed. - autocurve = kwds.get("autocurve", None) - if autocurve or ( - autocurve is None - and "edge_curved" not in kwds - and "curved" not in graph.edge_attributes() - and graph.ecount() < 10000 - ): - from igraph import autocurve - - default = kwds.get("edge_curved", 0) - if default is True: - default = 0.5 - default = float(default) - kwds["edge_curved"] = autocurve(graph, attribute=None, default=default) - - # Construct the vertex, edge and label drawers - vertex_drawer = self.vertex_drawer_factory(context, bbox, palette, layout) - edge_drawer = self.edge_drawer_factory(context, palette) - label_drawer = self.label_drawer_factory(context) - - # Construct the visual vertex/edge builders based on the specifications - # provided by the vertex_drawer and the edge_drawer - vertex_builder = vertex_drawer.VisualVertexBuilder(graph.vs, kwds) - edge_builder = edge_drawer.VisualEdgeBuilder(graph.es, kwds) - - # Determine the order in which we will draw the vertices and edges - vertex_order = self._determine_vertex_order(graph, kwds) - edge_order = self._determine_edge_order(graph, kwds) - - # Draw the highlighted groups (if any) - if "mark_groups" in kwds: - mark_groups = kwds["mark_groups"] - - # Deferred import to avoid a cycle in the import graph - from igraph.clustering import VertexClustering, VertexCover - - # Figure out what to do with mark_groups in order to be able to - # iterate over it and get memberlist-color pairs - if isinstance(mark_groups, dict): - # Dictionary mapping vertex indices or tuples of vertex - # indices to colors - group_iter = iter(mark_groups.items()) - elif isinstance(mark_groups, (VertexClustering, VertexCover)): - # Vertex clustering - group_iter = ((group, color) for color, group in enumerate(mark_groups)) - elif hasattr(mark_groups, "__iter__"): - # Lists, tuples, iterators etc - group_iter = iter(mark_groups) - else: - # False - group_iter = iter({}.items()) - - # We will need a polygon drawer to draw the convex hulls - polygon_drawer = PolygonDrawer(context, bbox) - - # Iterate over color-memberlist pairs - for group, color_id in group_iter: - if not group or color_id is None: - continue - - color = palette.get(color_id) - - if isinstance(group, VertexSeq): - group = [vertex.index for vertex in group] - if not hasattr(group, "__iter__"): - raise TypeError("group membership list must be iterable") - - # Get the vertex indices that constitute the convex hull - hull = [group[i] for i in convex_hull([layout[idx] for idx in group])] - - # Calculate the preferred rounding radius for the corners - corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) - - # Construct the polygon - polygon = [layout[idx] for idx in hull] - - if len(polygon) == 2: - # Expand the polygon (which is a flat line otherwise) - a, b = Point(*polygon[0]), Point(*polygon[1]) - c = corner_radius * (a - b).normalized() - n = Point(-c[1], c[0]) - polygon = [a + n, b + n, b - c, b - n, a - n, a + c] - else: - # Expand the polygon around its center of mass - center = Point( - *[sum(coords) / float(len(coords)) for coords in zip(*polygon)] - ) - polygon = [ - Point(*point).towards(center, -corner_radius) - for point in polygon - ] - - # Draw the hull - context.set_source_rgba(color[0], color[1], color[2], color[3] * 0.25) - polygon_drawer.draw_path(polygon, corner_radius=corner_radius) - context.fill_preserve() - context.set_source_rgba(*color) - context.stroke() - - # Construct the iterator that we will use to draw the edges - es = graph.es - if edge_order is None: - # Default edge order - edge_coord_iter = zip(es, edge_builder) - else: - # Specified edge order - edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) - - # Draw the edges - if directed: - drawer_method = edge_drawer.draw_directed_edge - else: - drawer_method = edge_drawer.draw_undirected_edge - for edge, visual_edge in edge_coord_iter: - src, dest = edge.tuple - src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] - drawer_method(visual_edge, src_vertex, dest_vertex) - - # Construct the iterator that we will use to draw the vertices - vs = graph.vs - if vertex_order is None: - # Default vertex order - vertex_coord_iter = zip(vs, vertex_builder, layout) - else: - # Specified vertex order - vertex_coord_iter = ( - (vs[i], vertex_builder[i], layout[i]) for i in vertex_order - ) - - # Draw the vertices - drawer_method = vertex_drawer.draw - context.set_line_width(1) - for vertex, visual_vertex, coords in vertex_coord_iter: - drawer_method(visual_vertex, vertex, coords) - - # Decide whether the labels have to be wrapped - wrap = kwds.get("wrap_labels") - if wrap is None: - wrap = Configuration.instance()["plotting.wrap_labels"] - wrap = bool(wrap) - - # Construct the iterator that we will use to draw the vertex labels - if vertex_order is None: - # Default vertex order - vertex_coord_iter = zip(vertex_builder, layout) - else: - # Specified vertex order - vertex_coord_iter = ((vertex_builder[i], layout[i]) for i in vertex_order) - - # Draw the vertex labels - for vertex, coords in vertex_coord_iter: - if vertex.label is None: - continue - - # Set the font family, size, color and text - context.select_font_face( - vertex.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL - ) - context.set_font_size(vertex.label_size) - context.set_source_rgba(*vertex.label_color) - label_drawer.text = vertex.label - - if vertex.label_dist: - # Label is displaced from the center of the vertex. - _, yb, w, h, _, _ = label_drawer.text_extents() - w, h = w / 2.0, h / 2.0 - radius = vertex.label_dist * vertex.size / 2.0 - # First we find the reference point that is at distance `radius' - # from the vertex in the direction given by `label_angle'. - # Then we place the label in a way that the line connecting the - # center of the bounding box of the label with the center of the - # vertex goes through the reference point and the reference - # point lies exactly on the bounding box of the vertex. - alpha = vertex.label_angle % (2 * pi) - cx = coords[0] + radius * cos(alpha) - cy = coords[1] - radius * sin(alpha) - # Now we have the reference point. We have to decide which side - # of the label box will intersect with the line that connects - # the center of the label with the center of the vertex. - if w > 0: - beta = atan2(h, w) % (2 * pi) - else: - beta = pi / 2.0 - gamma = pi - beta - if alpha > 2 * pi - beta or alpha <= beta: - # Intersection at left edge of label - cx += w - cy -= tan(alpha) * w - elif alpha > beta and alpha <= gamma: - # Intersection at bottom edge of label - try: - cx += h / tan(alpha) - except Exception: - pass # tan(alpha) == inf - cy -= h - elif alpha > gamma and alpha <= gamma + 2 * beta: - # Intersection at right edge of label - cx -= w - cy += tan(alpha) * w - else: - # Intersection at top edge of label - try: - cx -= h / tan(alpha) - except Exception: - pass # tan(alpha) == inf - cy += h - # Draw the label - label_drawer.draw_at(cx - w, cy - h - yb, wrap=wrap) - else: - # Label is exactly in the center of the vertex - cx, cy = coords - half_size = vertex.size / 2.0 - label_drawer.bbox = ( - cx - half_size, - cy - half_size, - cx + half_size, - cy + half_size, - ) - label_drawer.draw(wrap=wrap) - - # Construct the iterator that we will use to draw the edge labels - es = graph.es - if edge_order is None: - # Default edge order - edge_coord_iter = zip(es, edge_builder) - else: - # Specified edge order - edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) - - # Draw the edge labels - for edge, visual_edge in edge_coord_iter: - if visual_edge.label is None: - continue - - # Set the font family, size, color and text - context.select_font_face( - visual_edge.font, cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL - ) - context.set_font_size(visual_edge.label_size) - context.set_source_rgba(*visual_edge.label_color) - label_drawer.text = visual_edge.label - - # Ask the edge drawer to propose an anchor point for the label - src, dest = edge.tuple - src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] - (x, y), (halign, valign) = edge_drawer.get_label_position( - edge, src_vertex, dest_vertex - ) - - # Measure the text - _, yb, w, h, _, _ = label_drawer.text_extents() - w /= 2.0 - h /= 2.0 - - # Place the text relative to the edge - if halign == TextAlignment.RIGHT: - x -= w - elif halign == TextAlignment.LEFT: - x += w - if valign == TextAlignment.BOTTOM: - y -= h - yb / 2.0 - elif valign == TextAlignment.TOP: - y += h - - # Draw the edge label - label_drawer.halign = halign - label_drawer.valign = valign - label_drawer.bbox = (x - w, y - h, x + w, y + h) - label_drawer.draw(wrap=wrap) - - -##################################################################### - - -class UbiGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): - """Graph drawer that draws a given graph on an UbiGraph display - using the XML-RPC API of UbiGraph. - - The following vertex attributes are supported: C{color}, C{label}, - C{shape}, C{size}. See the Ubigraph documentation for supported shape - names. Sizes are relative to the default Ubigraph size. - - The following edge attributes are supported: C{color}, C{label}, - C{width}. Edge widths are relative to the default Ubigraph width. - - All color specifications supported by igraph (e.g., color names, - palette indices, RGB triplets, RGBA quadruplets, HTML format) - are understood by the Ubigraph graph drawer. - - The drawer also has two attributes, C{vertex_defaults} and - C{edge_defaults}. These are dictionaries that can be used to - set default values for the vertex/edge attributes in Ubigraph. - - @deprecated: UbiGraph has not received updates since 2008 and is now not - available for download (at least not from the official sources). - The UbiGraph graph drawer will be removed from python-igraph in - 0.10.0. - """ - - def __init__(self, url="http://localhost:20738/RPC2"): - """Constructs an UbiGraph drawer using the display at the given - URL.""" - super().__init__(url, "ubigraph") - self.vertex_defaults = dict(color="#ff0000", shape="cube", size=1.0) - self.edge_defaults = dict(color="#ffffff", width=1.0) - - warn( - "UbiGraphDrawer is deprecated from python-igraph 0.9.4", - DeprecationWarning - ) - - def draw(self, graph, *args, **kwds): - """Draws the given graph on an UbiGraph display. - - @keyword clear: whether to clear the current UbiGraph display before - plotting. Default: C{True}.""" - display = self.service - - # Clear the display and set the default visual attributes - if kwds.get("clear", True): - display.clear() - - for k, v in self.vertex_defaults.items(): - display.set_vertex_style_attribute(0, k, str(v)) - for k, v in self.edge_defaults.items(): - display.set_edge_style_attribute(0, k, str(v)) - - # Custom color converter function - def color_conv(color): - return color_to_html_format(color_name_to_rgb(color)) - - # Construct the visual vertex/edge builders - class VisualVertexBuilder(AttributeCollectorBase): - """Collects some visual properties of a vertex for drawing""" - - _kwds_prefix = "vertex_" - color = (str(self.vertex_defaults["color"]), color_conv) - label = None - shape = str(self.vertex_defaults["shape"]) - size = float(self.vertex_defaults["size"]) - - class VisualEdgeBuilder(AttributeCollectorBase): - """Collects some visual properties of an edge for drawing""" - - _kwds_prefix = "edge_" - color = (str(self.edge_defaults["color"]), color_conv) - label = None - width = float(self.edge_defaults["width"]) - - vertex_builder = VisualVertexBuilder(graph.vs, kwds) - edge_builder = VisualEdgeBuilder(graph.es, kwds) - - # Add the vertices - n = graph.vcount() - new_vertex = display.new_vertex - vertex_ids = [new_vertex() for _ in range(n)] - - # Add the edges - new_edge = display.new_edge - eids = [ - new_edge(vertex_ids[edge.source], vertex_ids[edge.target]) - for edge in graph.es - ] - - # Add arrowheads if needed - if graph.is_directed(): - display.set_edge_style_attribute(0, "arrow", "true") - - # Set the vertex attributes - set_attr = display.set_vertex_attribute - vertex_defaults = self.vertex_defaults - for vertex_id, vertex in zip(vertex_ids, vertex_builder): - if vertex.color != vertex_defaults["color"]: - set_attr(vertex_id, "color", vertex.color) - if vertex.label: - set_attr(vertex_id, "label", str(vertex.label)) - if vertex.shape != vertex_defaults["shape"]: - set_attr(vertex_id, "shape", vertex.shape) - if vertex.size != vertex_defaults["size"]: - set_attr(vertex_id, "size", str(vertex.size)) - - # Set the edge attributes - set_attr = display.set_edge_attribute - edge_defaults = self.edge_defaults - for edge_id, edge in zip(eids, edge_builder): - if edge.color != edge_defaults["color"]: - set_attr(edge_id, "color", edge.color) - if edge.label: - set_attr(edge_id, "label", edge.label) - if edge.width != edge_defaults["width"]: - set_attr(edge_id, "width", str(edge.width)) - - -##################################################################### +__all__ = ("CytoscapeGraphDrawer",) class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): @@ -680,6 +63,14 @@ def draw(self, graph, name="Network from igraph", create_view=True, *args, **kwd simply uses the vertex index for each vertex.""" from xmlrpc.client import Fault + # Positional arguments are not used + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, + ) + cy = self.service # Create the network @@ -798,7 +189,7 @@ def fetch(self, name=None, directed=False, keep_canonical_names=False): version = tuple(map(int, version.split(".")[:2])) if version < (1, 3): raise NotImplementedError( - "CytoscapeGraphDrawer requires " "Cytoscape-RPC 1.3 or newer" + "CytoscapeGraphDrawer requires Cytoscape-RPC 1.3 or newer" ) # Find out the ID of the network we are interested in @@ -956,399 +347,12 @@ def draw(self, graph, *args, **kwds): - ``encoder`` lets one specify an instance of ``json.JSONEncoder`` that will be used to encode the JSON objects. """ - self.streamer.post(graph, self.connection, encoder=kwds.get("encoder")) - - -##################################################################### - - -class MatplotlibGraphDrawer(AbstractGraphDrawer): - """Graph drawer that uses a pyplot.Axes as context""" - - _shape_dict = { - "rectangle": "s", - "circle": "o", - "hidden": "none", - "triangle-up": "^", - "triangle-down": "v", - } - - def __init__(self, ax): - """Constructs the graph drawer and associates it with the mpl axes""" - self.ax = ax - - def draw(self, graph, *args, **kwds): - # NOTE: matplotlib has numpy as a dependency, so we can use it in here - from collections import defaultdict - import matplotlib as mpl - import matplotlib.markers as mmarkers - from matplotlib.path import Path - from matplotlib.patches import FancyArrowPatch - from matplotlib.patches import ArrowStyle - import numpy as np - - # Deferred import to avoid a cycle in the import graph - from igraph.clustering import VertexClustering, VertexCover - - def shrink_vertex(ax, aux, vcoord, vsize_squared): - """Shrink edge by vertex size""" - aux_display, vcoord_display = ax.transData.transform([aux, vcoord]) - d = sqrt(((aux_display - vcoord_display) ** 2).sum()) - fr = sqrt(vsize_squared) / d - end_display = vcoord_display + fr * (aux_display - vcoord_display) - end = ax.transData.inverted().transform(end_display) - return end - - def callback_factory(ax, vcoord, vsizes, arrows): - def callback_edge_offset(event): - for arrow, src, tgt in arrows: - v1, v2 = vcoord[src], vcoord[tgt] - # This covers both cases (curved and straight) - aux1, aux2 = arrow._path_original.vertices[[1, -2]] - start = shrink_vertex(ax, aux1, v1, vsizes[src]) - end = shrink_vertex(ax, aux2, v2, vsizes[tgt]) - arrow._path_original.vertices[0] = start - arrow._path_original.vertices[-1] = end - - return callback_edge_offset - - ax = self.ax - - # FIXME: deal with unnamed *args - - # graph is not necessarily a graph, it can be a VertexClustering. If so - # extract the graph. The clustering itself can be overridden using - # the "mark_groups" option - clustering = None - if isinstance(graph, (VertexClustering, VertexCover)): - clustering = graph - graph = clustering.graph - - # Get layout - layout = kwds.get("layout", graph.layout()) - if isinstance(layout, str): - layout = graph.layout(layout) - - # Vertex coordinates - vcoord = layout.coords - - # mark groups: the used data structure is eventually the dict option: - # tuples of vertex indices as the keys, colors as the values. We - # convert other formats into that one here - if "mark_groups" not in kwds: - kwds["mark_groups"] = False - if kwds["mark_groups"] is False: - pass - elif (kwds["mark_groups"] is True) and (clustering is not None): - pass - elif isinstance(kwds["mark_groups"], (VertexClustering, VertexCover)): - if clustering is not None: - raise ValueError( - "mark_groups cannot override a clustering with another" - ) - else: - clustering = kwds["mark_groups"] - kwds["mark_groups"] = True - else: - try: - mg_iter = iter(kwds["mark_groups"]) - except TypeError: - raise TypeError("mark_groups is not in the right format") - kwds["mark_groups"] = dict(mg_iter) - - # If a clustering is set and marks are requested but without a specific - # colormap, make the colormap - - # Two things need coloring: vertices and groups/clusters (polygon) - # The coloring needs to be coordinated between the two. - if clustering is not None: - # If mark_groups is a dict, we don't need a default color dict, we - # can just use the mark_groups dict. If mark_groups is False and - # vertex_color is set, we don't need either because the colors are - # already fully specified. In all other cases, we need a default - # color dict. - if isinstance(kwds["mark_groups"], dict): - group_colordict = kwds["mark_groups"] - elif (kwds["mark_groups"] is False) and ("vertex_color" in kwds): - pass - else: - membership = clustering.membership - if isinstance(clustering, VertexCover): - membership = [x[0] for x in membership] - clusters = sorted(set(membership)) - n_colors = len(clusters) - cmap = mpl.cm.get_cmap("viridis") - colors = [cmap(1.0 * i / n_colors) for i in range(n_colors)] - cluster_colordict = {g: c for g, c in zip(clusters, colors)} - - # mark_groups if not explicitly marked - group_colordict = defaultdict(list) - for i, g in enumerate(membership): - color = cluster_colordict[g] - group_colordict[color].append(i) - del cluster_colordict - # Invert keys and values - group_colordict = {tuple(v): k for k, v in group_colordict.items()} - - # If mark_groups is set but not defined, make a default colormap - if kwds["mark_groups"] is True: - kwds["mark_groups"] = group_colordict - - if "vertex_color" not in kwds: - kwds["vertex_color"] = ['none' for m in membership] - for group_vs, color in group_colordict.items(): - for i in group_vs: - kwds["vertex_color"][i] = color - - # Now mark_groups is either a dict or False - # If vertex_color is not set, we can rely on mark_groups if a dict, - # else we need to make up the same colormap as if we were requested groups - if "vertex_color" not in kwds: - if isinstance(kwds["mark_groups"], dict): - membership = clustering.membership - if isinstance(clustering, VertexCover): - membership = [x[0] for x in membership] - - # Mark groups - if "mark_groups" in kwds and isinstance(kwds["mark_groups"], dict): - for idx, color in kwds["mark_groups"].items(): - points = [vcoord[i] for i in idx] - vertices = np.asarray(convex_hull(points, coords=True)) - # 15% expansion - vertices += 0.15 * (vertices - vertices.mean(axis=0)) - - # NOTE: we could include a corner cutting algorithm close to - # the vertices (e.g. Chaikin) for beautification, or a corner - # radius like it's done in the Cairo interface - polygon = mpl.patches.Polygon( - vertices, - facecolor=color, - alpha=0.3, - zorder=0, - edgecolor=color, - lw=2, - ) - ax.add_artist(polygon) - - # Vertex properties - nv = graph.vcount() - - # Vertex size - vsizes = kwds.get("vertex_size", 5) - # Enforce numpy array for sizes, because (1) we need the square and (2) - # they are needed to calculate autoshrinking of edges - if np.isscalar(vsizes): - vsizes = np.repeat(vsizes, nv) - else: - vsizes = np.asarray(vsizes) - # ax.scatter uses the *square* of diameter - vsizes **= 2 - - # Vertex color - c = kwds.get("vertex_color", "steelblue") - - # Vertex opacity - alpha = kwds.get("alpha", 1.0) - - # Vertex labels - label = kwds.get("vertex_label", None) - - # Vertex label size - label_size = kwds.get("vertex_label_size", mpl.rcParams["font.size"]) - - # Vertex zorder - vzorder = kwds.get("vertex_order", 2) - - # Vertex shapes - # mpl shapes use slightly different names from Cairo, but we want the - # API to feel consistent, so we use a conversion dictionary - shapes = kwds.get("vertex_shape", "o") - if shapes is not None: - if isinstance(shapes, str): - shapes = self._shape_dict.get(shapes, shapes) - elif isinstance(shapes, mmarkers.MarkerStyle): - pass - - # Scatter vertices - x, y = list(zip(*vcoord)) - ax.scatter(x, y, s=vsizes, c=c, marker=shapes, zorder=vzorder, alpha=alpha) - - # Vertex labels - if label is not None: - for i, lab in enumerate(label): - xi, yi = x[i], y[i] - ax.text(xi, yi, lab, fontsize=label_size) - - dx = max(x) - min(x) - dy = max(y) - min(y) - ax.set_xlim(min(x) - 0.05 * dx, max(x) + 0.05 * dx) - ax.set_ylim(min(y) - 0.05 * dy, max(y) + 0.05 * dy) - - # Edge properties - ne = graph.ecount() - ec = kwds.get("edge_color", "black") - edge_width = kwds.get("edge_width", 1) - arrow_width = kwds.get("edge_arrow_width", 2) - arrow_length = kwds.get("edge_arrow_size", 4) - ealpha = kwds.get("edge_alpha", 1.0) - ezorder = kwds.get("edge_order", 1.0) - try: - ezorder = float(ezorder) - ezorder = [ezorder] * ne - except TypeError: - pass - - # Decide whether we need to calculate the curvature of edges - # automatically -- and calculate them if needed. - autocurve = kwds.get("autocurve", None) - if autocurve or ( - autocurve is None - and "edge_curved" not in kwds - and "curved" not in graph.edge_attributes() - and graph.ecount() < 10000 - ): - from igraph import autocurve - - default = kwds.get("edge_curved", 0) - if default is True: - default = 0.5 - default = float(default) - ecurved = autocurve(graph, attribute=None, default=default) - elif "edge_curved" in kwds: - ecurved = kwds["edge_curved"] - elif "curved" in graph.edge_attributes(): - ecurved = graph.es["curved"] - else: - ecurved = [0] * ne - - # Arrow style for directed and undirected graphs - if graph.is_directed(): - arrowstyle = ArrowStyle( - "-|>", - head_length=arrow_length, - head_width=arrow_width, - ) - else: - arrowstyle = "-" - - # Edge coordinates and curvature - nloops = [0 for x in range(ne)] - arrows = [] - for ie, edge in enumerate(graph.es): - src, tgt = edge.source, edge.target - x1, y1 = vcoord[src] - x2, y2 = vcoord[tgt] - - # Loops require special treatment - if src == tgt: - # Find all non-loop edges - nloopstot = 0 - angles = [] - for tgtn in graph.neighbors(src): - if tgtn == src: - nloopstot += 1 - continue - xn, yn = vcoord[tgtn] - angles.append(180.0 / pi * atan2(yn - y1, xn - x1) % 360) - # with .neighbors(mode=ALL), which is default, loops are double - # counted - nloopstot //= 2 - angles = sorted(set(angles)) - - # Only loops or one non-loop - if len(angles) < 2: - ashift = angles[0] if angles else 270 - if nloopstot == 1: - # Only one self loop, use a quadrant only - angles = [(ashift + 135) % 360, (ashift + 225) % 360] - else: - nshift = 360.0 / nloopstot - angles = [ - (ashift + nshift * nloops[src]) % 360, - (ashift + nshift * (nloops[src] + 1)) % 360, - ] - nloops[src] += 1 - else: - angles.append(angles[0] + 360) - idiff = 0 - diff = 0 - for i in range(len(angles) - 1): - diffi = abs(angles[i + 1] - angles[i]) - if diffi > diff: - idiff = i - diff = diffi - angles = angles[idiff : idiff + 2] - ashift = angles[0] - nshift = (angles[1] - angles[0]) / nloopstot - angles = [ - (ashift + nshift * nloops[src]), - (ashift + nshift * (nloops[src] + 1)), - ] - nloops[src] += 1 - - # this is not great, but alright - angspan = angles[1] - angles[0] - if angspan < 180: - angmid1 = angles[0] + 0.1 * angspan - angmid2 = angles[1] - 0.1 * angspan - else: - angmid1 = angles[0] + 0.5 * (angspan - 180) + 45 - angmid2 = angles[1] - 0.5 * (angspan - 180) - 45 - aux1 = ( - x1 + 0.2 * dx * cos(pi / 180 * angmid1), - y1 + 0.2 * dy * sin(pi / 180 * angmid1), - ) - aux2 = ( - x1 + 0.2 * dx * cos(pi / 180 * angmid2), - y1 + 0.2 * dy * sin(pi / 180 * angmid2), - ) - start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) - end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) - - path = Path( - [start, aux1, aux2, end], - # Cubic bezier by mpl - codes=[1, 4, 4, 4], - ) - - else: - curved = ecurved[ie] - if curved: - aux1 = (2 * x1 + x2) / 3.0 - curved * 0.5 * (y2 - y1), ( - 2 * y1 + y2 - ) / 3.0 + curved * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - curved * 0.5 * (y2 - y1), ( - y1 + 2 * y2 - ) / 3.0 + curved * 0.5 * (x2 - x1) - start = shrink_vertex(ax, aux1, (x1, y1), vsizes[src]) - end = shrink_vertex(ax, aux2, (x2, y2), vsizes[tgt]) - - path = Path( - [start, aux1, aux2, end], - # Cubic bezier by mpl - codes=[1, 4, 4, 4], - ) - else: - start = shrink_vertex(ax, (x2, y2), (x1, y1), vsizes[src]) - end = shrink_vertex(ax, (x1, y1), (x2, y2), vsizes[tgt]) - - path = Path([start, end], codes=[1, 2]) - - arrow = FancyArrowPatch( - path=path, - arrowstyle=arrowstyle, - lw=edge_width, - color=ec, - alpha=ealpha, - zorder=ezorder[ie], + # Positional arguments are not used + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, ) - ax.add_artist(arrow) - - # Store arrows and their sources and targets for autoscaling - arrows.append((arrow, src, tgt)) - # Autoscaling during zoom, figure resize, reset axis limits - callback = callback_factory(ax, vcoord, vsizes, arrows) - ax.get_figure().canvas.mpl_connect("resize_event", callback) - ax.callbacks.connect("xlim_changed", callback) - ax.callbacks.connect("ylim_changed", callback) + self.streamer.post(graph, self.connection, encoder=kwds.get("encoder")) diff --git a/src/igraph/drawing/matplotlib/__init__.py b/src/igraph/drawing/matplotlib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/igraph/drawing/matplotlib/dendrogram.py b/src/igraph/drawing/matplotlib/dendrogram.py new file mode 100644 index 000000000..8be43b231 --- /dev/null +++ b/src/igraph/drawing/matplotlib/dendrogram.py @@ -0,0 +1,176 @@ +""" +Drawing routines to draw the matrices. + +This module provides implementations of matrix drawers. +""" + +from igraph.drawing.baseclasses import AbstractDrawer +from igraph.drawing.utils import str_to_orientation + +from .utils import find_matplotlib + +__all__ = ("MatplotlibDendrogramDrawer",) + +mpl, _ = find_matplotlib() + + +class MatplotlibDendrogramDrawer(AbstractDrawer): + """Matplotlib drawer object for dendrograms.""" + + def __init__(self, ax): + """Constructs the drawer and associates it to the given Axes. + + @param ax: the Axes on which we will draw + """ + super().__init__() + self.context = ax + + def _plot_item(self, dendro, ax, orientation, idx, x, y): + """Plots a dendrogram item to the given Cairo context + + @param context: the Cairo context we are plotting on + @param horiz: whether the dendrogram is horizontally oriented + @param idx: the index of the item + @param x: the X position of the item + @param y: the Y position of the item + """ + if dendro._names is None or dendro._names[idx] is None: + return + + if orientation == "lr": + ha, va, rotation = "right", "center", 0 + elif orientation == "rl": + ha, va, rotation = "left", "center", 0 + elif orientation == "tb": + ha, va, rotation = "center", "bottom", 90 + else: + ha, va, rotation = "center", "top", 90 + + # TODO: offset a little? But remember zoom in callbacks + + ax.text( + x, + y, + dendro._names[idx], + ha=ha, + va=va, + rotation=rotation, + ) + + def draw(self, dendro, orientation="lr", **kwds): + """Draws the given Dendrogram in a matplotlib Axes. + + @param dendro: the igraph.Dendrogram to plot. + @orientation: the direction of the plot. Accepted values are "lr" (root + on the right), "rl" (root on the left), "tb" (root at the bottom), + and "bt" (root at the top). A few aliases are available (see + L{igraph.utils.str_to_orientation}). + + Other keyword arguments are passed to mpl.patches.Polygon. + """ + from igraph.layout import Layout + + ax = self.context + + # Styling defaults + kwds["edgecolor"] = kwds.pop("color", "black") + if ("lw" not in kwds) and ("linewidth" not in kwds): + kwds["linewidth"] = 1 + + if dendro._names is None: + dendro._names = [str(x) for x in range(dendro._nitems)] + + orientation = str_to_orientation(orientation, reversed_vertical=True) + horiz = orientation in ("lr", "rl") + + # Calculate node coordinates + layout = Layout([(0, 0)] * dendro._nitems, dim=2) + inorder = dendro._traverse_inorder() + if not horiz: + x, y = 0, 0 + # Leaves + for idx, element in enumerate(inorder): + layout[element] = (x, 0) + x += 1 + + # Internal nodes + for id1, id2 in dendro._merges: + x = (layout[id1][0] + layout[id2][0]) / 2.0 + # TODO: this is a little restrictive, but alright + # for such a simple layout. More complex tree layouts + # should be in a separate Layout anyway + y += 1 + layout.append((x, y)) + + # Mirror or rotate the layout if necessary + if orientation == "bt": + layout.mirror(1) + else: + x, y = 0, 0 + for idx, element in enumerate(inorder): + layout[element] = (0, y) + y += 1 + + for id1, id2 in dendro._merges: + y = (layout[id1][1] + layout[id2][1]) / 2.0 + # TODO: this is a little restrictive, but alright + # for such a simple layout. More complex tree layouts + # should be in a separate Layout anyway + x += 1 + layout.append((x, y)) + + # Mirror or rotate the layout if necessary + if orientation == "rl": + layout.mirror(0) + + # Draw leaf names + # + # for idx in range(dendro._nitems): + # x, y = layout[idx] + # self._plot_item(dendro, ax, orientation, idx, x, y) + ticks, ticklabels = [], [] + for idx in range(dendro._nitems): + x, y = layout[idx] + if not horiz: + ticks.append(x) + else: + ticks.append(y) + ticklabels.append(dendro._names[idx]) + + if not horiz: + ax.set_xticks(ticks) + ax.set_xticklabels(ticklabels) + ax.set_yticks([]) + else: + ax.set_yticks(ticks) + ax.set_yticklabels(ticklabels) + ax.set_xticks([]) + + # Draw dendrogram lines + # Each path is a U-shaped fork + if not horiz: + for idx, (id1, id2) in enumerate(dendro._merges): + x0, y0 = layout[id1] + x1, y1 = layout[id2] + x2, y2 = layout[idx + dendro._nitems] + poly = mpl.patches.Polygon( + [[x0, y0], [x0, y2], [x1, y2], [x1, y1]], + closed=False, + facecolor="none", + **kwds, + ) + ax.add_patch(poly) + else: + for idx, (id1, id2) in enumerate(dendro._merges): + x0, y0 = layout[id1] + x1, y1 = layout[id2] + x2, y2 = layout[idx + dendro._nitems] + poly = mpl.patches.Polygon( + [[x0, y0], [x2, y0], [x2, y1], [x1, y1]], + closed=False, + facecolor="none", + **kwds, + ) + ax.add_patch(poly) + + ax.autoscale_view() diff --git a/src/igraph/drawing/matplotlib/edge.py b/src/igraph/drawing/matplotlib/edge.py new file mode 100644 index 000000000..e2998fac6 --- /dev/null +++ b/src/igraph/drawing/matplotlib/edge.py @@ -0,0 +1,274 @@ +"""Drawers for various edge styles in Matplotlib graph plots.""" + +from math import atan2, cos, pi, sin + +from igraph.drawing.baseclasses import AbstractEdgeDrawer +from igraph.drawing.metamagic import AttributeCollectorBase +from igraph.drawing.matplotlib.utils import find_matplotlib +from igraph.drawing.utils import euclidean_distance, intersect_bezier_curve_and_circle + +__all__ = ("MatplotlibEdgeDrawer",) + +mpl, plt = find_matplotlib() + + +class MatplotlibEdgeDrawer(AbstractEdgeDrawer): + """Matplotlib-specific abstract edge drawer object.""" + + def __init__(self, context, palette): + """Constructs the edge drawer. + + @param context: a Matplotlib axes object on which the edges will be + drawn. + @param palette: the palette that can be used to map integer color + indices to colors when drawing edges + """ + self.context = context + self.palette = palette + self.VisualEdgeBuilder = self._construct_visual_edge_builder() + + def _construct_visual_edge_builder(self): + """Construct the visual edge builder that will collect the visual + attributes of an edge when it is being drawn.""" + + class VisualEdgeBuilder(AttributeCollectorBase): + """Builder that collects some visual properties of an edge for + drawing""" + + _kwds_prefix = "edge_" + arrow_size = 0.007 + arrow_width = 1.4 + color = ("#444", self.palette.get) + curved = (0.0, self._curvature_to_float) + label = None + label_color = ("black", self.palette.get) + label_size = 12.0 + font = "sans-serif" + width = 2.0 + + return VisualEdgeBuilder + + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + if src_vertex == dest_vertex: # TODO + return self.draw_loop_edge(edge, src_vertex) + + ax = self.context + (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position + (x_src, y_src), (x_dest, y_dest) = src_vertex.position, dest_vertex.position + + # Draw the edge + path = {"vertices": [], "codes": []} + path["vertices"].append([x1, y1]) + path["codes"].append("MOVETO") + + if edge.curved: + # Calculate the curve + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + + # Coordinates of the control points of the Bezier curve + xc1, yc1 = aux1 + xc2, yc2 = aux2 + + # Determine where the edge intersects the circumference of the + # vertex shape: Tip of the arrow + x2, y2 = intersect_bezier_curve_and_circle( + x_src, y_src, xc1, yc1, xc2, yc2, x_dest, y_dest, dest_vertex.size / 2.0 + ) + + # Calculate the arrow head coordinates + angle = atan2(y_dest - y2, x_dest - x2) # navid + arrow_size = 15.0 * edge.arrow_size + arrow_width = 10.0 / edge.arrow_width + aux_points = [ + ( + x2 - arrow_size * cos(angle - pi / arrow_width), + y2 - arrow_size * sin(angle - pi / arrow_width), + ), + ( + x2 - arrow_size * cos(angle + pi / arrow_width), + y2 - arrow_size * sin(angle + pi / arrow_width), + ), + ] + + # Midpoint of the base of the arrow triangle + x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( + aux_points[0][1] + aux_points[1][1] + ) / 2.0 + + # Vector representing the base of the arrow triangle + x_arrow_base_vec, y_arrow_base_vec = ( + aux_points[0][0] - aux_points[1][0] + ), (aux_points[0][1] - aux_points[1][1]) + + # Recalculate the curve such that it lands on the base of the arrow triangle + aux1 = (2 * x_src + x_arrow_mid) / 3.0 - edge.curved * 0.5 * ( + y_arrow_mid - y_src + ), (2 * y_src + y_arrow_mid) / 3.0 + edge.curved * 0.5 * ( + x_arrow_mid - x_src + ) + aux2 = (x_src + 2 * x_arrow_mid) / 3.0 - edge.curved * 0.5 * ( + y_arrow_mid - y_src + ), (y_src + 2 * y_arrow_mid) / 3.0 + edge.curved * 0.5 * ( + x_arrow_mid - x_src + ) + + # Offset the second control point (aux2) such that it falls precisely + # on the normal to the arrow base vector. Strictly speaking, + # offset_length is the offset length divided by the length of the + # arrow base vector. + offset_length = (x_arrow_mid - aux2[0]) * x_arrow_base_vec + ( + y_arrow_mid - aux2[1] + ) * y_arrow_base_vec + offset_length /= ( + euclidean_distance(0, 0, x_arrow_base_vec, y_arrow_base_vec) ** 2 + ) + + aux2 = ( + aux2[0] + x_arrow_base_vec * offset_length, + aux2[1] + y_arrow_base_vec * offset_length, + ) + + # Draw the curve from the first vertex to the midpoint of the base + # of the arrow head + path["vertices"].append(aux1) + path["vertices"].append(aux2) + path["vertices"].append([x_arrow_mid, y_arrow_mid]) + path["codes"].extend(["CURVE4"] * 3) + + else: + # Determine where the edge intersects the circumference of the + # vertex shape. + x2, y2 = dest_vertex.shape.intersection_point( + x2, y2, x1, y1, dest_vertex.size + ) + + # Draw the arrowhead + angle = atan2(y_dest - y2, x_dest - x2) + arrow_size = 15.0 * edge.arrow_size + arrow_width = 10.0 / edge.arrow_width + aux_points = [ + ( + x2 - arrow_size * cos(angle - pi / arrow_width), + y2 - arrow_size * sin(angle - pi / arrow_width), + ), + ( + x2 - arrow_size * cos(angle + pi / arrow_width), + y2 - arrow_size * sin(angle + pi / arrow_width), + ), + ] + + # Midpoint of the base of the arrow triangle + x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( + aux_points[0][1] + aux_points[1][1] + ) / 2.0 + # Draw the line + path["vertices"].append([x_arrow_mid, y_arrow_mid]) + path["codes"].append("LINETO") + + # Draw the edge + stroke = mpl.patches.PathPatch( + mpl.path.Path( + path["vertices"], + codes=[getattr(mpl.path.Path, x) for x in path["codes"]], + ), + edgecolor=edge.color, + facecolor="none", + linewidth=edge.width, + ) + ax.add_patch(stroke) + + # Draw the arrow head + arrowhead = mpl.patches.Polygon( + [ + [x2, y2], + aux_points[0], + aux_points[1], + ], + closed=True, + facecolor=edge.color, + edgecolor="none", + ) + ax.add_patch(arrowhead) + + def draw_loop_edge(self, edge, vertex): + """Draws a loop edge. + + The default implementation draws a small circle. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param vertex: the vertex to which the edge is attached. Visual + properties are given again as attributes. + """ + ax = self.context + radius = vertex.size * 1.5 + center_x = vertex.position[0] + cos(pi / 4) * radius / 2.0 + center_y = vertex.position[1] - sin(pi / 4) * radius / 2.0 + stroke = mpl.patches.Arc( + (center_x, center_y), + radius / 2.0, + radius / 2.0, + theta1=0, + theta2=360.0, + linewidth=edge.width, + facecolor="none", + edgecolor=edge.color, + ) + # FIXME: make a PathCollection?? + ax.add_patch(stroke) + + def draw_undirected_edge(self, edge, src_vertex, dest_vertex): + """Draws an undirected edge. + + The default implementation of this method draws undirected edges + as straight lines. Loop edges are drawn as small circles. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param src_vertex: the source vertex. Visual properties are given + again as attributes. + @param dest_vertex: the target vertex. Visual properties are given + again as attributes. + """ + if src_vertex == dest_vertex: + return self.draw_loop_edge(edge, src_vertex) + + ax = self.context + + path = {"vertices": [], "codes": []} + path["vertices"].append(src_vertex.position) + path["codes"].append("MOVETO") + + if edge.curved: + (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + + path["vertices"].append(aux1) + path["vertices"].append(aux2) + path["vertices"].append(dest_vertex.position) + path["codes"].extend(["CURVE4"] * 3) + else: + path["vertices"].append(dest_vertex.position) + path["codes"].append("LINETO") + + stroke = mpl.patches.PathPatch( + mpl.path.Path( + path["vertices"], + codes=[getattr(mpl.path.Path, x) for x in path["codes"]], + ), + edgecolor=edge.color, + facecolor="none", + linewidth=edge.width, + ) + # FIXME: make a PathCollection?? + ax.add_artist(stroke) diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py new file mode 100644 index 000000000..4578249e2 --- /dev/null +++ b/src/igraph/drawing/matplotlib/graph.py @@ -0,0 +1,317 @@ +""" +Drawing routines to draw graphs. + +This module contains routines to draw graphs on: + + - Cairo surfaces (L{DefaultGraphDrawer}) + - Matplotlib axes (L{MatplotlibGraphDrawer}) + +It also contains routines to send an igraph graph directly to +(U{Cytoscape}) using the +(U{CytoscapeRPC plugin}), see +L{CytoscapeGraphDrawer}. L{CytoscapeGraphDrawer} can also fetch the current +network from Cytoscape and convert it to igraph format. +""" + +from warnings import warn + +from igraph._igraph import convex_hull, VertexSeq +from igraph.drawing.baseclasses import AbstractGraphDrawer +from igraph.drawing.utils import Point + +from .edge import MatplotlibEdgeDrawer +from .polygon import MatplotlibPolygonDrawer +from .utils import find_matplotlib +from .vertex import MatplotlibVertexDrawer + +__all__ = ("MatplotlibGraphDrawer",) + +_, plt = find_matplotlib() + +##################################################################### + + +class MatplotlibGraphDrawer(AbstractGraphDrawer): + """Graph drawer that uses a pyplot.Axes as context""" + + _shape_dict = { + "rectangle": "s", + "circle": "o", + "hidden": "none", + "triangle-up": "^", + "triangle-down": "v", + } + + def __init__( + self, + ax, + vertex_drawer_factory=MatplotlibVertexDrawer, + edge_drawer_factory=MatplotlibEdgeDrawer, + ): + """Constructs the graph drawer and associates it with the mpl Axes + + @param ax: the matplotlib Axes to draw into. + @param vertex_drawer_factory: a factory method that returns an + L{AbstractVertexDrawer} instance bound to the given Matplotlib axes. + The factory method must take three parameters: the axes and the + palette to be used for drawing colored vertices, and the layout of + the graph. The default vertex drawer is L{MatplotlibVertexDrawer}. + @param edge_drawer_factory: a factory method that returns an + L{AbstractEdgeDrawer} instance bound to a given Matplotlib Axes. + The factory method must take two parameters: the Axes and the palette + to be used for drawing colored edges. The default edge drawer is + L{MatplotlibEdgeDrawer}. + """ + self.ax = ax + self.vertex_drawer_factory = vertex_drawer_factory + self.edge_drawer_factory = edge_drawer_factory + + def draw(self, graph, *args, **kwds): + # Deferred import to avoid a cycle in the import graph + from igraph.clustering import VertexClustering, VertexCover + + # Positional arguments are not used + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, + ) + + # Some abbreviations for sake of simplicity + directed = graph.is_directed() + ax = self.ax + + # Palette + palette = kwds.pop("palette", None) + + # Calculate/get the layout of the graph + layout = self.ensure_layout(kwds.get("layout", None), graph) + + # Decide whether we need to calculate the curvature of edges + # automatically -- and calculate them if needed. + autocurve = kwds.get("autocurve", None) + if autocurve or ( + autocurve is None + and "edge_curved" not in kwds + and "curved" not in graph.edge_attributes() + and graph.ecount() < 10000 + ): + from igraph import autocurve + + default = kwds.get("edge_curved", 0) + if default is True: + default = 0.5 + default = float(default) + kwds["edge_curved"] = autocurve( + graph, + attribute=None, + default=default, + ) + + # Construct the vertex, edge and label drawers + vertex_drawer = self.vertex_drawer_factory(ax, palette, layout) + edge_drawer = self.edge_drawer_factory(ax, palette) + + # Construct the visual vertex/edge builders based on the specifications + # provided by the vertex_drawer and the edge_drawer + vertex_builder = vertex_drawer.VisualVertexBuilder(graph.vs, kwds) + edge_builder = edge_drawer.VisualEdgeBuilder(graph.es, kwds) + + # Draw the highlighted groups (if any) + if "mark_groups" in kwds: + mark_groups = kwds["mark_groups"] + + # Deferred import to avoid a cycle in the import graph + from igraph.clustering import VertexClustering, VertexCover + + # Figure out what to do with mark_groups in order to be able to + # iterate over it and get memberlist-color pairs + if isinstance(mark_groups, dict): + # Dictionary mapping vertex indices or tuples of vertex + # indices to colors + group_iter = iter(mark_groups.items()) + elif isinstance(mark_groups, (VertexClustering, VertexCover)): + # Vertex clustering + group_iter = ((group, color) for color, group in enumerate(mark_groups)) + elif hasattr(mark_groups, "__iter__"): + # Lists, tuples, iterators etc + group_iter = iter(mark_groups) + else: + # False + group_iter = iter({}.items()) + + if kwds.get("legend", False): + legend_info = { + "handles": [], + "labels": [], + } + + # Iterate over color-memberlist pairs + for group, color_id in group_iter: + if not group or color_id is None: + continue + + color = palette.get(color_id) + + if isinstance(group, VertexSeq): + group = [vertex.index for vertex in group] + if not hasattr(group, "__iter__"): + raise TypeError("group membership list must be iterable") + + # Get the vertex indices that constitute the convex hull + hull = [group[i] for i in convex_hull([layout[idx] for idx in group])] + + # Calculate the preferred rounding radius for the corners + corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) + + # Construct the polygon + polygon = [layout[idx] for idx in hull] + + if len(polygon) == 2: + # Expand the polygon (which is a flat line otherwise) + a, b = Point(*polygon[0]), Point(*polygon[1]) + c = corner_radius * (a - b).normalized() + n = Point(-c[1], c[0]) + polygon = [a + n, b + n, b - c, b - n, a - n, a + c] + else: + # Expand the polygon around its center of mass + center = Point( + *[sum(coords) / float(len(coords)) for coords in zip(*polygon)] + ) + polygon = [ + Point(*point).towards(center, -corner_radius) + for point in polygon + ] + + # Draw the hull + facecolor = (color[0], color[1], color[2], 0.25 * color[3]) + drawer = MatplotlibPolygonDrawer(ax) + drawer.draw( + polygon, + corner_radius=corner_radius, + facecolor=facecolor, + edgecolor=color, + ) + + if kwds.get("legend", False): + legend_info["handles"].append( + plt.Rectangle( + (0, 0), + 0, + 0, + facecolor=facecolor, + edgecolor=color, + ) + ) + legend_info["labels"].append(str(color_id)) + + if kwds.get("legend", False): + ax.legend( + legend_info["handles"], + legend_info["labels"], + ) + + # Determine the order in which we will draw the vertices and edges + vertex_order = self._determine_vertex_order(graph, kwds) + edge_order = self._determine_edge_order(graph, kwds) + + # Construct the iterator that we will use to draw the vertices + vs = graph.vs + if vertex_order is None: + # Default vertex order + vertex_coord_iter = zip(vs, vertex_builder, layout) + else: + # Specified vertex order + vertex_coord_iter = ( + (vs[i], vertex_builder[i], layout[i]) for i in vertex_order + ) + + # Draw the vertices + drawer_method = vertex_drawer.draw + for vertex, visual_vertex, coords in vertex_coord_iter: + drawer_method(visual_vertex, vertex, coords) + + # Construct the iterator that we will use to draw the vertex labels + vs = graph.vs + if vertex_order is None: + # Default vertex order + vertex_coord_iter = zip(vertex_builder, layout) + else: + # Specified vertex order + vertex_coord_iter = ((vertex_builder[i], layout[i]) for i in vertex_order) + + # Draw the vertex labels + for vertex, coords in vertex_coord_iter: + if vertex.label is None: + continue + + label_size = kwds.get( + "vertex_label_size", + vertex.label_size, + ) + + ax.text( + *coords, + vertex.label, + fontsize=label_size, + # TODO: alignment, overlap, offset, etc. + ) + + # Construct the iterator that we will use to draw the edges + es = graph.es + if edge_order is None: + # Default edge order + edge_coord_iter = zip(es, edge_builder) + else: + # Specified edge order + edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) + + # Draw the edges + if directed: + drawer_method = edge_drawer.draw_directed_edge + else: + drawer_method = edge_drawer.draw_undirected_edge + for edge, visual_edge in edge_coord_iter: + src, dest = edge.tuple + src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] + drawer_method(visual_edge, src_vertex, dest_vertex) + + # Draw the edge labels + labels = kwds.get("edge_label", None) + if labels is not None: + edge_label_iter = ( + (labels[i], edge_builder[i], graph.es[i]) for i in range(graph.ecount()) + ) + for label, visual_edge, edge in edge_label_iter: + # Ask the edge drawer to propose an anchor point for the label + src, dest = edge.tuple + src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] + (x, y), (halign, valign) = edge_drawer.get_label_position( + edge, + src_vertex, + dest_vertex, + ) + + ax.text( + x, + y, + label, + fontsize=visual_edge.label_size, + color=visual_edge.label_color, + ha=halign, + va=valign, + # TODO: offset, etc. + ) + + # Despine + ax.spines["right"].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["left"].set_visible(False) + ax.spines["bottom"].set_visible(False) + + # Remove axis ticks + ax.set_xticks([]) + ax.set_yticks([]) + + ax.autoscale_view() diff --git a/src/igraph/drawing/matplotlib/histogram.py b/src/igraph/drawing/matplotlib/histogram.py new file mode 100644 index 000000000..0084875c1 --- /dev/null +++ b/src/igraph/drawing/matplotlib/histogram.py @@ -0,0 +1,37 @@ +"""This module provides implementation for a Matplotlib-specific histogram drawer.""" + +from igraph.drawing.baseclasses import AbstractDrawer + +__all__ = ("MatplotlibHistogramDrawer",) + + +class MatplotlibHistogramDrawer(AbstractDrawer): + """Matplotlib drawer object for matrices.""" + + def __init__(self, ax): + """Constructs the drawer and associates it to the given Axes. + + @param ax: the Axes on which we will draw + """ + self.context = ax + + def draw(self, matrix, **kwds): + """Draws the given Matrix in a matplotlib Axes. + + @param matrix: the igraph.Histogram to plot. + + """ + ax = self.context + + xmin = kwds.get("min", self._min) + ymin = 0 + xmax = kwds.get("max", self._max) + ymax = kwds.get("max_value", max(self._bins)) + width = self._bin_width + + x = [self._min + width * i for i, _ in enumerate(self._bins)] + y = self._bins + # Draw the boxes/bars + ax.bar(x, y, align='left') + ax.set_xlim(xmin, xmax) + ax.set_ylim(ymin, ymax) diff --git a/src/igraph/drawing/matplotlib/matrix.py b/src/igraph/drawing/matplotlib/matrix.py new file mode 100644 index 000000000..5a255ee08 --- /dev/null +++ b/src/igraph/drawing/matplotlib/matrix.py @@ -0,0 +1,26 @@ +"""This module provides implementation for a Matplotlib-specific matrix drawer.""" + +from igraph.drawing.baseclasses import AbstractDrawer + +__all__ = ("MatplotlibMatrixDrawer",) + + +class MatplotlibMatrixDrawer(AbstractDrawer): + """Matplotlib drawer object for matrices.""" + + def __init__(self, ax): + """Constructs the drawer and associates it to the given Axes. + + @param ax: the Axes on which we will draw + """ + self.context = ax + + def draw(self, matrix, **kwds): + """Draws the given Matrix in a matplotlib Axes. + + @param matrix: the igraph.Matrix to plot. + + Keyword arguments are passed to Axes.imshow. + """ + ax = self.context + ax.imshow(matrix.data, interpolation="nearest", **kwds) diff --git a/src/igraph/drawing/matplotlib/palette.py b/src/igraph/drawing/matplotlib/palette.py new file mode 100644 index 000000000..fb9fc3850 --- /dev/null +++ b/src/igraph/drawing/matplotlib/palette.py @@ -0,0 +1,49 @@ +"""This module provides implementation for a Matplotlib-specific palette drawer.""" + +from igraph.drawing.baseclasses import AbstractDrawer + +__all__ = ("MatplotlibPaletteDrawer",) + + +class MatplotlibPaletteDrawer(AbstractDrawer): + """Matplotlib drawer object for matrices.""" + + def __init__(self, ax): + """Constructs the drawer and associates it to the given Axes. + + @param ax: the Axes on which we will draw + """ + self.context = ax + + def draw(self, matrix, **kwds): + """Draws the given Matrix in a matplotlib Axes. + + @param matrix: the igraph.Histogram to plot. + + """ + from igraph.datatypes import Matrix + from igraph.drawing.utils import find_matplotlib, str_to_orientation + + mpl, _ = find_matplotlib() + ax = self.context + + orientation = str_to_orientation(kwds.get("orientation", "lr")) + + # Construct a matrix and plot that + indices = list(range(len(self))) + if orientation in ("rl", "bt"): + indices.reverse() + if orientation in ("lr", "rl"): + matrix = Matrix([indices]) + else: + matrix = Matrix([[i] for i in indices]) + + cmap = mpl.colors.ListedColormap( + [self.get(i) for i in range(self.length)], + ) + matrix.__plot__( + 'matplotlib', + ax, + cmap=cmap, + **kwds, + ) diff --git a/src/igraph/drawing/matplotlib/polygon.py b/src/igraph/drawing/matplotlib/polygon.py new file mode 100644 index 000000000..9267b806d --- /dev/null +++ b/src/igraph/drawing/matplotlib/polygon.py @@ -0,0 +1,88 @@ +from igraph.drawing.utils import calculate_corner_radii +from igraph.utils import consecutive_pairs + +from .utils import find_matplotlib + +__all__ = ("MatplotlibPolygonDrawer",) + +mpl, plt = find_matplotlib() + + +class MatplotlibPolygonDrawer: + """Class that is used to draw polygons in matplotlib. + + The corner points of the polygon can be set by the C{points} + property of the drawer, or passed at construction time. Most + drawing methods in this class also have an extra C{points} + argument that can be used to override the set of points in the + C{points} property.""" + + def __init__(self, ax): + """Constructs a new polygon drawer that draws on the given + Matplotlib axes. + + @param ax: the matplotlib Axes to draw on + @param points: the list of corner points + """ + self.context = ax + + def draw(self, points, corner_radius=0, **kwds): + """Draws a polygon to the associated axes. + + @param points: the coordinates of the corners of the polygon, + in clockwise or counter-clockwise order, or C{None} if we are + about to use the C{points} property of the class. + @param corner_radius: if zero, an ordinary polygon will be drawn. + If positive, the corners of the polygon will be rounded with + the given radius. + """ + if len(points) < 2: + # Well, a polygon must have at least two corner points + return + + ax = self.context + if corner_radius <= 0: + # No rounded corners, this is simple + stroke = mpl.patches.Polygon(points, **kwds) + ax.add_patch(stroke) + + # Rounded corners. First, we will take each side of the + # polygon and find what the corner radius should be on + # each corner. If the side is longer than 2r (where r is + # equal to corner_radius), the radius allowed by that side + # is r; if the side is shorter, the radius is the length + # of the side / 2. For each corner, the final corner radius + # is the smaller of the radii on the two sides adjacent to + # the corner. + corner_radii = calculate_corner_radii(points, corner_radius) + + # Okay, move to the last corner, adjusted by corner_radii[-1] + # towards the first corner + path = [] + codes = [] + path.append((points[-1].towards(points[0], corner_radii[-1]))) + codes.append(mpl.path.Path.MOVETO) + + # Now, for each point in points, draw a line towards the + # corner, stopping before it in a distance of corner_radii[idx], + # then draw the corner + u = points[-1] + for idx, (v, w) in enumerate(consecutive_pairs(points, True)): + radius = corner_radii[idx] + path.append(v.towards(u, radius)) + codes.append(mpl.path.Path.LINETO) + + aux1 = v.towards(u, radius / 2) + aux2 = v.towards(w, radius / 2) + + path.append(aux1) + path.append(aux2) + path.append(v.towards(w, corner_radii[idx])) + codes.extend([mpl.path.Path.CURVE4] * 3) + u = v + + stroke = mpl.patches.PathPatch( + mpl.path.Path(path, codes=codes, closed=True), + **kwds, + ) + ax.add_patch(stroke) diff --git a/src/igraph/drawing/matplotlib/utils.py b/src/igraph/drawing/matplotlib/utils.py new file mode 100644 index 000000000..3c842c656 --- /dev/null +++ b/src/igraph/drawing/matplotlib/utils.py @@ -0,0 +1,26 @@ +from igraph.drawing.utils import FakeModule +from typing import Any + +__all__ = ("find_matplotlib", ) + + +def find_matplotlib() -> Any: + """Tries to import the ``matplotlib`` Python module if it is installed. + Returns a fake module if everything fails. + """ + try: + import matplotlib as mpl + + has_mpl = True + except ImportError: + mpl = FakeModule("You need to install matplotlib to use this functionality") + has_mpl = False + + if has_mpl: + import matplotlib.pyplot as plt + else: + plt = FakeModule( + "You need to install matplotlib.pyplot to use this functionality" + ) + + return mpl, plt diff --git a/src/igraph/drawing/matplotlib/vertex.py b/src/igraph/drawing/matplotlib/vertex.py new file mode 100644 index 000000000..3a7db0d90 --- /dev/null +++ b/src/igraph/drawing/matplotlib/vertex.py @@ -0,0 +1,71 @@ +"""This module provides implementations of Matplotlib-specific vertex drawers, +i.e. drawers that the Matplotlib graph drawer will use to draw vertices. +""" + +from math import pi + +from igraph.drawing.baseclasses import AbstractVertexDrawer +from igraph.drawing.metamagic import AttributeCollectorBase +from igraph.drawing.shapes import ShapeDrawerDirectory + +__all__ = ("MatplotlibVertexDrawer",) + + +class MatplotlibVertexDrawer(AbstractVertexDrawer): + """Matplotlib backend-specific vertex drawer.""" + + def __init__(self, ax, palette, layout): + self.context = ax + super().__init__(palette, layout) + self.VisualVertexBuilder = self._construct_visual_vertex_builder() + + def _construct_visual_vertex_builder(self): + class VisualVertexBuilder(AttributeCollectorBase): + """Collects some visual properties of a vertex for drawing""" + + _kwds_prefix = "vertex_" + color = ("red", self.palette.get) + frame_color = ("black", self.palette.get) + frame_width = 1.0 + label = None + label_angle = -pi / 2 + label_dist = 0.0 + label_color = ("black", self.palette.get) + font = "sans-serif" + label_size = 12.0 + # FIXME? mpl.rcParams["font.size"]) + position = dict(func=self.layout.__getitem__) + shape = ("circle", ShapeDrawerDirectory.resolve_default) + size = 0.2 + width = None + height = None + zorder = 2 + + return VisualVertexBuilder + + def draw(self, visual_vertex, vertex, coords): + ax = self.context + + width = ( + visual_vertex.width + if visual_vertex.width is not None + else visual_vertex.size + ) + height = ( + visual_vertex.height + if visual_vertex.height is not None + else visual_vertex.size + ) + + stroke = visual_vertex.shape.draw_path( + ax, + coords[0], + coords[1], + width, + height, + facecolor=visual_vertex.color, + edgecolor=visual_vertex.frame_color, + linewidth=visual_vertex.frame_width, + zorder=visual_vertex.zorder, + ) + ax.add_patch(stroke) diff --git a/src/igraph/drawing/plotly/__init__.py b/src/igraph/drawing/plotly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/igraph/drawing/plotly/edge.py b/src/igraph/drawing/plotly/edge.py new file mode 100644 index 000000000..410b04d5f --- /dev/null +++ b/src/igraph/drawing/plotly/edge.py @@ -0,0 +1,270 @@ +"""Drawers for various edge styles in Matplotlib graph plots.""" + +from math import atan2, cos, pi, sin + +from igraph.drawing.baseclasses import AbstractEdgeDrawer +from igraph.drawing.metamagic import AttributeCollectorBase +from igraph.drawing.plotly.utils import find_plotly, format_path_step, format_arc, format_rgba +from igraph.drawing.utils import Point, euclidean_distance, intersect_bezier_curve_and_circle + +__all__ = ("PlotlyEdgeDrawer",) + +plotly = find_plotly() + + +class PlotlyEdgeDrawer(AbstractEdgeDrawer): + """Matplotlib-specific abstract edge drawer object.""" + + def __init__(self, context, palette): + """Constructs the edge drawer. + + @param context: a plotly Figure object on which the edges will be + drawn. + @param palette: the palette that can be used to map integer color + indices to colors when drawing edges + """ + self.context = context + self.palette = palette + self.VisualEdgeBuilder = self._construct_visual_edge_builder() + + def _construct_visual_edge_builder(self): + """Construct the visual edge builder that will collect the visual + attributes of an edge when it is being drawn.""" + + class VisualEdgeBuilder(AttributeCollectorBase): + """Builder that collects some visual properties of an edge for + drawing""" + + _kwds_prefix = "edge_" + arrow_size = 0.007 + arrow_width = 1.4 + color = "#444" + curved = (0.0, self._curvature_to_float) + label = None + label_color = ("black", self.palette.get) + label_size = 12.0 + font = "sans-serif" + width = 2.0 + + return VisualEdgeBuilder + + def draw_directed_edge(self, edge, src_vertex, dest_vertex): + if src_vertex == dest_vertex: # TODO + return self.draw_loop_edge(edge, src_vertex) + + fig = self.context + (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position + (x_src, y_src), (x_dest, y_dest) = src_vertex.position, dest_vertex.position + + # Draw the edge + path = [ + format_path_step("M", [x1, y1]), + ] + + if edge.curved: + # Calculate the curve + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + + # Coordinates of the control points of the Bezier curve + xc1, yc1 = aux1 + xc2, yc2 = aux2 + + # Determine where the edge intersects the circumference of the + # vertex shape: Tip of the arrow + x2, y2 = intersect_bezier_curve_and_circle( + x_src, y_src, xc1, yc1, xc2, yc2, x_dest, y_dest, dest_vertex.size / 2.0 + ) + + # Calculate the arrow head coordinates + angle = atan2(y_dest - y2, x_dest - x2) # navid + arrow_size = 15.0 * edge.arrow_size + arrow_width = 10.0 / edge.arrow_width + aux_points = [ + ( + x2 - arrow_size * cos(angle - pi / arrow_width), + y2 - arrow_size * sin(angle - pi / arrow_width), + ), + ( + x2 - arrow_size * cos(angle + pi / arrow_width), + y2 - arrow_size * sin(angle + pi / arrow_width), + ), + ] + + # Midpoint of the base of the arrow triangle + x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( + aux_points[0][1] + aux_points[1][1] + ) / 2.0 + + # Vector representing the base of the arrow triangle + x_arrow_base_vec, y_arrow_base_vec = ( + aux_points[0][0] - aux_points[1][0] + ), (aux_points[0][1] - aux_points[1][1]) + + # Recalculate the curve such that it lands on the base of the arrow triangle + aux1 = (2 * x_src + x_arrow_mid) / 3.0 - edge.curved * 0.5 * ( + y_arrow_mid - y_src + ), (2 * y_src + y_arrow_mid) / 3.0 + edge.curved * 0.5 * ( + x_arrow_mid - x_src + ) + aux2 = (x_src + 2 * x_arrow_mid) / 3.0 - edge.curved * 0.5 * ( + y_arrow_mid - y_src + ), (y_src + 2 * y_arrow_mid) / 3.0 + edge.curved * 0.5 * ( + x_arrow_mid - x_src + ) + + # Offset the second control point (aux2) such that it falls precisely + # on the normal to the arrow base vector. Strictly speaking, + # offset_length is the offset length divided by the length of the + # arrow base vector. + offset_length = (x_arrow_mid - aux2[0]) * x_arrow_base_vec + ( + y_arrow_mid - aux2[1] + ) * y_arrow_base_vec + offset_length /= ( + euclidean_distance(0, 0, x_arrow_base_vec, y_arrow_base_vec) ** 2 + ) + + aux2 = ( + aux2[0] + x_arrow_base_vec * offset_length, + aux2[1] + y_arrow_base_vec * offset_length, + ) + + # Draw the curve from the first vertex to the midpoint of the base + # of the arrow head + path.append(format_path_step( + "C", [aux1, aux2, [x_arrow_mid, y_arrow_mid]] + )) + + else: + # FIXME: this is tricky in plotly, let's skip for now + ## Determine where the edge intersects the circumference of the + ## vertex shape. + #x2, y2 = dest_vertex.shape.intersection_point( + # x2, y2, x1, y1, dest_vertex.size + #) + + # Draw the arrowhead + angle = atan2(y_dest - y2, x_dest - x2) + arrow_size = 15.0 * edge.arrow_size + arrow_width = 10.0 / edge.arrow_width + aux_points = [ + ( + x2 - arrow_size * cos(angle - pi / arrow_width), + y2 - arrow_size * sin(angle - pi / arrow_width), + ), + ( + x2 - arrow_size * cos(angle + pi / arrow_width), + y2 - arrow_size * sin(angle + pi / arrow_width), + ), + ] + + # Midpoint of the base of the arrow triangle + x_arrow_mid, y_arrow_mid = (aux_points[0][0] + aux_points[1][0]) / 2.0, ( + aux_points[0][1] + aux_points[1][1] + ) / 2.0 + # Draw the line + path.append(format_path_step( + "L", Point(x_arrow_mid, y_arrow_mid), + )) + + path = ' '.join(path) + + # Draw the edge + stroke = dict( + type='path', + path=path, + line_color=format_rgba(edge.color), + line_width=edge.width, + ) + fig.add_shape(stroke) + + # Draw the arrow head + arrowhead = plotly.graph_objects.Scatter( + x=[x2, aux_points[0][0], aux_points[1][0], x2], + y=[y2, aux_points[0][1], aux_points[1][1], y2], + fillcolor=format_rgba(edge.color), + mode="lines", + ) + fig.add_trace(arrowhead) + + def draw_loop_edge(self, edge, vertex): + """Draws a loop edge. + + The default implementation draws a small circle. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param vertex: the vertex to which the edge is attached. Visual + properties are given again as attributes. + """ + fig = self.context + radius = vertex.size * 1.5 + center_x = vertex.position[0] + cos(pi / 4) * radius / 2.0 + center_y = vertex.position[1] - sin(pi / 4) * radius / 2.0 + stroke = dict( + type='path', + path=format_arc( + (center_x, center_y), + radius / 2.0, + radius / 2.0, + theta1=0, + theta2=360.0, + ), + line_color=edge.color, + line_width=edge.width, + ) + fig.add_shape(stroke) + + def draw_undirected_edge(self, edge, src_vertex, dest_vertex): + """Draws an undirected edge. + + The default implementation of this method draws undirected edges + as straight lines. Loop edges are drawn as small circles. + + @param edge: the edge to be drawn. Visual properties of the edge + are defined by the attributes of this object. + @param src_vertex: the source vertex. Visual properties are given + again as attributes. + @param dest_vertex: the target vertex. Visual properties are given + again as attributes. + """ + if src_vertex == dest_vertex: + return self.draw_loop_edge(edge, src_vertex) + + fig = self.context + + path = [ + format_path_step("M", src_vertex.position) + ] + + if edge.curved: + (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position + aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + + path.append(format_path_step( + "C", [aux1, aux2, dest_vertex.position], + )) + + else: + path.append(format_path_step( + "L", dest_vertex.position, + )) + + path = ' '.join(path) + + stroke = dict( + type='path', + path=path, + line_color=format_rgba(edge.color), + line_width=edge.width, + ) + fig.add_shape(stroke) diff --git a/src/igraph/drawing/plotly/graph.py b/src/igraph/drawing/plotly/graph.py new file mode 100644 index 000000000..e45e7d3db --- /dev/null +++ b/src/igraph/drawing/plotly/graph.py @@ -0,0 +1,281 @@ +""" +Drawing routines to draw graphs. + +This module contains routines to draw graphs on plotly surfaces. + +""" + +from collections import defaultdict +from warnings import warn + +from igraph._igraph import convex_hull, VertexSeq +from igraph.drawing.baseclasses import AbstractGraphDrawer +from igraph.drawing.utils import Point + +from .edge import PlotlyEdgeDrawer +from .polygon import PlotlyPolygonDrawer +from .utils import find_plotly, format_rgba +from .vertex import PlotlyVerticesDrawer + +__all__ = ("PlotlyGraphDrawer",) + +plotly = find_plotly() + +##################################################################### + + +class PlotlyGraphDrawer(AbstractGraphDrawer): + """Graph drawer that uses a pyplot.Axes as context""" + + # These need conversions, plus default passthrough for arbitrary + # plotly shapes + _shape_dict = { + "rectangle": "square", + "hidden": "none", + } + + def __init__( + self, + fig, + vertex_drawer_factory=PlotlyVerticesDrawer, + edge_drawer_factory=PlotlyEdgeDrawer, + ): + """Constructs the graph drawer and associates it with the plotly Figure + + @param fig: the plotly.graph_objects.Figure to draw into. + + """ + self.fig = fig + self.vertex_drawer_factory = vertex_drawer_factory + self.edge_drawer_factory = edge_drawer_factory + + def draw(self, graph, *args, **kwds): + # Deferred import to avoid a cycle in the import graph + from igraph.clustering import VertexClustering, VertexCover + + # Positional arguments are not used + if args: + warn( + "Positional arguments to plot functions are ignored " + "and will be deprecated soon.", + DeprecationWarning, + ) + + # Some abbreviations for sake of simplicity + directed = graph.is_directed() + fig = self.fig + + # Palette + palette = kwds.pop("palette", None) + + # Calculate/get the layout of the graph + layout = self.ensure_layout(kwds.get("layout", None), graph) + + # Decide whether we need to calculate the curvature of edges + # automatically -- and calculate them if needed. + autocurve = kwds.get("autocurve", None) + if autocurve or ( + autocurve is None + and "edge_curved" not in kwds + and "curved" not in graph.edge_attributes() + and graph.ecount() < 10000 + ): + from igraph import autocurve + + default = kwds.get("edge_curved", 0) + if default is True: + default = 0.5 + default = float(default) + kwds["edge_curved"] = autocurve( + graph, + attribute=None, + default=default, + ) + + # Construct the vertex, edge and label drawers + vertex_drawer = self.vertex_drawer_factory(fig, palette, layout) + edge_drawer = self.edge_drawer_factory(fig, palette) + + # Construct the visual edge builders based on the specifications + # provided by the edge_drawer + vertex_builder = vertex_drawer.VisualVertexBuilder(graph.vs, kwds) + edge_builder = edge_drawer.VisualEdgeBuilder(graph.es, kwds) + + # Draw the highlighted groups (if any) + if "mark_groups" in kwds: + mark_groups = kwds["mark_groups"] + + # Deferred import to avoid a cycle in the import graph + from igraph.clustering import VertexClustering, VertexCover + + # Figure out what to do with mark_groups in order to be able to + # iterate over it and get memberlist-color pairs + if isinstance(mark_groups, dict): + # Dictionary mapping vertex indices or tuples of vertex + # indices to colors + group_iter = iter(mark_groups.items()) + elif isinstance(mark_groups, (VertexClustering, VertexCover)): + # Vertex clustering + group_iter = ((group, color) for color, group in enumerate(mark_groups)) + elif hasattr(mark_groups, "__iter__"): + # Lists, tuples, iterators etc + group_iter = iter(mark_groups) + else: + # False + group_iter = iter({}.items()) + + # Iterate over color-memberlist pairs + for group, color_id in group_iter: + if not group or color_id is None: + continue + + color = palette.get(color_id) + + if isinstance(group, VertexSeq): + group = [vertex.index for vertex in group] + if not hasattr(group, "__iter__"): + raise TypeError("group membership list must be iterable") + + # Get the vertex indices that constitute the convex hull + hull = [group[i] for i in convex_hull([layout[idx] for idx in group])] + + # Calculate the preferred rounding radius for the corners + #FIXME + corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) + + # Construct the polygon + polygon = [layout[idx] for idx in hull] + + if len(polygon) == 2: + # Expand the polygon (which is a flat line otherwise) + a, b = Point(*polygon[0]), Point(*polygon[1]) + c = corner_radius * (a - b).normalized() + n = Point(-c[1], c[0]) + polygon = [a + n, b + n, b - c, b - n, a - n, a + c] + else: + # Expand the polygon around its center of mass + center = Point( + *[sum(coords) / float(len(coords)) for coords in zip(*polygon)] + ) + polygon = [ + Point(*point).towards(center, -corner_radius) + for point in polygon + ] + + # Draw the hull + facecolor = (color[0], color[1], color[2], 0.25 * color[3]) + drawer = PlotlyPolygonDrawer(fig) + drawer.draw( + polygon, + corner_radius=corner_radius, + fillcolor=format_rgba(facecolor), + line_color=format_rgba(color), + ) + + if kwds.get("legend", False): + # Proxy artist for legend + fig.add_trace( + plotly.graph_objects.Bar( + name=str(color_id), + x=[], y=[], + fillcolor=facecolor, + line_color=color, + ) + ) + + if kwds.get("legend", False): + fig.update_layout(showlegend=True) + + # Determine the order in which we will draw the vertices and edges + vertex_order = self._determine_vertex_order(graph, kwds) + edge_order = self._determine_edge_order(graph, kwds) + + # Construct the iterator that we will use to draw the vertices + vs = graph.vs + if vertex_order is None: + # Default vertex order + vertex_coord_iter = zip(vs, vertex_builder, layout) + else: + # Specified vertex order + vertex_coord_iter = ( + (vs[i], vertex_builder[i], layout[i]) for i in vertex_order + ) + + # Draw the vertices + drawer_method = vertex_drawer.draw + for vertex, visual_vertex, coords in vertex_coord_iter: + drawer_method(visual_vertex, vertex, coords) + + # Construct the iterator that we will use to draw the vertex labels + vs = graph.vs + if vertex_order is None: + # Default vertex order + vertex_coord_iter = zip(vertex_builder, layout) + else: + # Specified vertex order + vertex_coord_iter = ((vertex_builder[i], layout[i]) for i in vertex_order) + + # Draw the vertex labels + for vertex, coords in vertex_coord_iter: + vertex_drawer.draw_label(vertex, coords, **kwds) + + # Construct the iterator that we will use to draw the edges + es = graph.es + if edge_order is None: + # Default edge order + edge_coord_iter = zip(es, edge_builder) + else: + # Specified edge order + edge_coord_iter = ((es[i], edge_builder[i]) for i in edge_order) + + # Draw the edges and labels + # We need the vertex builder to get the layout and offsets + if directed: + drawer_method = edge_drawer.draw_directed_edge + else: + drawer_method = edge_drawer.draw_undirected_edge + for edge, visual_edge in edge_coord_iter: + src, dest = edge.tuple + src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] + drawer_method(visual_edge, src_vertex, dest_vertex) + + # Draw the edge labels + labels = kwds.get("edge_label", None) + if labels is not None: + edge_label_iter = ( + (labels[i], edge_builder[i], graph.es[i]) for i in range(graph.ecount()) + ) + lab_args = { + 'text': [], + 'x': [], + 'y': [], + 'color': [], + # FIXME: horizontal/vertical alignment, offset, etc? + } + for label, visual_edge, edge in edge_label_iter: + # Ask the edge drawer to propose an anchor point for the label + src, dest = edge.tuple + src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] + (x, y), (halign, valign) = edge_drawer.get_label_position( + edge, + src_vertex, + dest_vertex, + ) + if label is None: + continue + + lab_args['text'].append(label) + lab_args['x'].append(x) + lab_args['y'].append(y) + lab_args['color'].append(visual_edge.label_color) + stroke = plotly.graph_objects.Scatter( + mode='text', + **lab_args, + ) + fig.add_trace(stroke) + + # Despine + fig.update_layout( + yaxis={'visible': False, 'showticklabels': False}, + xaxis={'visible': False, 'showticklabels': False}, + ) diff --git a/src/igraph/drawing/plotly/polygon.py b/src/igraph/drawing/plotly/polygon.py new file mode 100644 index 000000000..c0978e42b --- /dev/null +++ b/src/igraph/drawing/plotly/polygon.py @@ -0,0 +1,104 @@ +from igraph.drawing.utils import calculate_corner_radii +from igraph.utils import consecutive_pairs + +from .utils import find_plotly, format_path_step + +__all__ = ("PlotlyPolygonDrawer",) + +plotly = find_plotly() + + +class PlotlyPolygonDrawer: + """Class that is used to draw polygons in matplotlib. + + The corner points of the polygon can be set by the C{points} + property of the drawer, or passed at construction time. Most + drawing methods in this class also have an extra C{points} + argument that can be used to override the set of points in the + C{points} property.""" + + def __init__(self, fig): + """Constructs a new polygon drawer that draws on the given + Matplotlib axes. + + @param fig: the plotly Figure to draw on + @param points: the list of corner points + """ + self.context = fig + + def draw(self, points, corner_radius=0, **kwds): + """Draws a polygon to the associated axes. + + @param points: the coordinates of the corners of the polygon, + in clockwise or counter-clockwise order, or C{None} if we are + about to use the C{points} property of the class. + @param corner_radius: if zero, an ordinary polygon will be drawn. + If positive, the corners of the polygon will be rounded with + the given radius. + """ + if len(points) < 2: + # Well, a polygon must have at least two corner points + return + + fig = self.context + if corner_radius <= 0: + # No rounded corners, this is simple + # We need to repeat the initial point to get a closed shape + x = [p[0] for p in points] + [points[0][0]] + y = [p[1] for p in points] + [points[0][1]] + kwds['mode'] = kwds.get('mode', 'line') + stroke = plotly.graph_objects.Scatter( + x=x, + y=y, + **kwds, + ) + fig.add_trace(stroke) + + # Rounded corners. First, we will take each side of the + # polygon and find what the corner radius should be on + # each corner. If the side is longer than 2r (where r is + # equal to corner_radius), the radius allowed by that side + # is r; if the side is shorter, the radius is the length + # of the side / 2. For each corner, the final corner radius + # is the smaller of the radii on the two sides adjacent to + # the corner. + corner_radii = calculate_corner_radii(points, corner_radius) + + # Okay, move to the last corner, adjusted by corner_radii[-1] + # towards the first corner + path = [] + path.append(format_path_step( + "M", + [points[-1].towards(points[0], corner_radii[-1])], + )) + + # Now, for each point in points, draw a line towards the + # corner, stopping before it in a distance of corner_radii[idx], + # then draw the corner + u = points[-1] + for idx, (v, w) in enumerate(consecutive_pairs(points, True)): + radius = corner_radii[idx] + path.append(format_path_step( + "L", + [v.towards(u, radius)], + )) + + aux1 = v.towards(u, radius / 2) + aux2 = v.towards(w, radius / 2) + + path.append(format_path_step( + "C", + [aux1, aux2, v.towards(w, corner_radii[idx])], + )) + u = v + + # Close path + path = "".join(path).strip(" ")+" Z" + stroke = dict( + type="path", + path=path, + **kwds, + ) + fig.update_layout( + shapes=[stroke], + ) diff --git a/src/igraph/drawing/plotly/utils.py b/src/igraph/drawing/plotly/utils.py new file mode 100644 index 000000000..be28dfc50 --- /dev/null +++ b/src/igraph/drawing/plotly/utils.py @@ -0,0 +1,74 @@ +from igraph.drawing.utils import FakeModule, Point +from typing import Any + +__all__ = ("find_plotly", ) + + +def find_plotly() -> Any: + """Tries to import the ``plotly`` Python module if it is installed. + Returns a fake module if everything fails. + """ + try: + import plotly + + except ImportError: + plotly = FakeModule("You need to install plotly to use this functionality") + + return plotly + + +def format_path_step(code, point_or_points): + """Format step in SVG path for plotly""" + if isinstance(point_or_points[0], (float, int)): + points = [point_or_points] + else: + points = point_or_points + + step = f"{code}" + for point in points: + x, y = point[0], point[1] + step += f" {x},{y}" + return step + + +# Adapted from https://community.plotly.com/t/arc-shape-with-path/7205/3 +def format_arc(center, radius_x, radius_y, theta1, theta2, N=100, closed=False): + """Approximation of an SVG-style arc + + NOTE: plotly does not currently support the native SVG "A/a" commands""" + import math + + xc, yc = center + dt = 1.0 * (theta2 - theta1) + t = [theta1 + dt * i / (N-1) for i in range(N)] + x = [xc + radius_x * math.cos(i) for i in t] + y = [yc + radius_y * math.sin(i) for i in t] + path = f'M {x[0]}, {y[0]}' + for k in range(1, len(t)): + path += f'L{x[k]}, {y[k]}' + if closed: + path += ' Z' + return path + + +def format_rgba(color): + """Format colors in a way understood by plotly""" + if isinstance(color, str): + return color + + if isinstance(color, float): + if color > 1: + color /= 255. + color = [color] * 3 + + r = int(255 * color[0]) + g = int(255 * color[1]) + b = int(255 * color[2]) + if len(color) > 3: + a = int(255 * color[3]) + else: + a = 255 + + colstr = f'rgba({r},{g},{b},{a})' + return colstr + diff --git a/src/igraph/drawing/plotly/vertex.py b/src/igraph/drawing/plotly/vertex.py new file mode 100644 index 000000000..4d6f6df52 --- /dev/null +++ b/src/igraph/drawing/plotly/vertex.py @@ -0,0 +1,95 @@ +"""Vertices drawer. Unlike other backends, all vertices are drawn at once""" + +from math import pi + +from igraph.drawing.baseclasses import AbstractVertexDrawer +from igraph.drawing.metamagic import AttributeCollectorBase +from .utils import find_plotly, format_rgba + +__all__ = ('PlotlyVerticesDrawer',) + +plotly = find_plotly() + + +class PlotlyVerticesDrawer(AbstractVertexDrawer): + """Plotly backend-specific vertex drawer.""" + + def __init__(self, fig, palette, layout): + self.fig = fig + super().__init__(palette, layout) + self.VisualVertexBuilder = self._construct_visual_vertex_builder() + + def _construct_visual_vertex_builder(self): + class VisualVertexBuilder(AttributeCollectorBase): + """Collects some visual properties of a vertex for drawing""" + + _kwds_prefix = "vertex_" + color = ("red", self.palette.get) + frame_color = ("black", self.palette.get) + frame_width = 1.0 + label = None + label_angle = -pi / 2 + label_dist = 0.0 + label_color = "black" + font = "sans-serif" + label_size = 12.0 + # FIXME? mpl.rcParams["font.size"]) + position = dict(func=self.layout.__getitem__) + shape = "circle" + size = 0.2 + width = None + height = None + zorder = 2 + + return VisualVertexBuilder + + + def draw(self, visual_vertex, vertex, point): + if visual_vertex.size <= 0: + return + + fig = self.fig + + marker_kwds = {} + marker_kwds['x'] = [point[0]] + marker_kwds['y'] = [point[1]] + marker_kwds['marker'] = { + 'symbol': visual_vertex.shape, + 'size': visual_vertex.size, + 'color': format_rgba(visual_vertex.color), + 'line_color': format_rgba(visual_vertex.frame_color), + } + + #if visual_vertex.label is not None: + # text_kwds['x'].append(point[0]) + # text_kwds['y'].append(point[1]) + # text_kwds['text'].append(visual_vertex.label) + + # Draw dots + stroke = plotly.graph_objects.Scatter( + mode='markers', + **marker_kwds, + ) + fig.add_trace(stroke) + + def draw_label(self, visual_vertex, point, **kwds): + if visual_vertex.label is None: + return + + fig = self.fig + + text_kwds = {} + text_kwds['x'] = [point[0]] + text_kwds['y'] = [point[1]] + text_kwds['text'].append(visual_vertex.label) + + # TODO: add more options + + # Draw text labels + stroke = plotly.graph_objects.Scatter( + mode='text', + **text_kwds, + ) + fig.add_trace(stroke) + + diff --git a/src/igraph/drawing/shapes.py b/src/igraph/drawing/shapes.py index a868cbf4a..79a274043 100644 --- a/src/igraph/drawing/shapes.py +++ b/src/igraph/drawing/shapes.py @@ -19,15 +19,16 @@ __all__ = ("ShapeDrawerDirectory",) +from abc import ABCMeta, abstractmethod from math import atan2, copysign, cos, pi, sin import sys -from igraph.drawing.baseclasses import AbstractCairoDrawer -from igraph.drawing.utils import Point -from igraph.utils import consecutive_pairs +from igraph.drawing.matplotlib.utils import find_matplotlib +mpl, plt = find_matplotlib() -class ShapeDrawer: + +class ShapeDrawer(metaclass=ABCMeta): """Static class, the ancestor of all vertex shape drawer classes. Custom shapes must implement at least the C{draw_path} method of the class. @@ -35,7 +36,8 @@ class ShapeDrawer: Cairo path appropriately.""" @staticmethod - def draw_path(ctx, center_x, center_y, width, height=None): + @abstractmethod + def draw_path(ctx, center_x, center_y, width, height=None, **kwargs): """Draws the path of the shape on the given Cairo context, without stroking or filling it. @@ -48,7 +50,7 @@ def draw_path(ctx, center_x, center_y, width, height=None): @param width: the width of the object @param height: the height of the object. If C{None}, equals to the width. """ - raise NotImplementedError("abstract class") + raise NotImplementedError @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): @@ -84,15 +86,20 @@ def draw_path(ctx, center_x, center_y, width, height=None): class RectangleDrawer(ShapeDrawer): """Static class which draws rectangular vertices""" - names = "rectangle rect rectangular square box" + names = "rectangle rect rectangular square box s" @staticmethod - def draw_path(ctx, center_x, center_y, width, height=None): + def draw_path(ctx, center_x, center_y, width, height=None, **kwargs): """Draws a rectangle-shaped path on the Cairo context without stroking or filling it. @see: ShapeDrawer.draw_path""" height = height or width - ctx.rectangle(center_x - width / 2, center_y - height / 2, width, height) + if isinstance(ctx, plt.Axes): + return mpl.patches.Rectangle( + (center_x - width / 2, center_y - height / 2), width, height, **kwargs + ) + else: + ctx.rectangle(center_x - width / 2, center_y - height / 2, width, height) @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): @@ -145,17 +152,20 @@ def intersection_point(center_x, center_y, source_x, source_y, width, height=Non class CircleDrawer(ShapeDrawer): """Static class which draws circular vertices""" - names = "circle circular" + names = "circle circular o" @staticmethod - def draw_path(ctx, center_x, center_y, width, height=None): + def draw_path(ctx, center_x, center_y, width, height=None, **kwargs): """Draws a circular path on the Cairo context without stroking or filling it. Height is ignored, it is the width that determines the diameter of the circle. @see: ShapeDrawer.draw_path""" - ctx.arc(center_x, center_y, width / 2, 0, 2 * pi) + if isinstance(ctx, plt.Axes): + return mpl.patches.Circle((center_x, center_y), width / 2, **kwargs) + else: + ctx.arc(center_x, center_y, width / 2, 0, 2 * pi) @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): @@ -172,19 +182,27 @@ def intersection_point(center_x, center_y, source_x, source_y, width, height=Non class UpTriangleDrawer(ShapeDrawer): """Static class which draws upright triangles""" - names = "triangle triangle-up up-triangle arrow arrow-up up-arrow" + names = "triangle triangle-up up-triangle arrow arrow-up up-arrow ^" @staticmethod - def draw_path(ctx, center_x, center_y, width, height=None): + def draw_path(ctx, center_x, center_y, width, height=None, **kwargs): """Draws an upright triangle on the Cairo context without stroking or filling it. @see: ShapeDrawer.draw_path""" height = height or width - ctx.move_to(center_x - width / 2, center_y + height / 2) - ctx.line_to(center_x, center_y - height / 2) - ctx.line_to(center_x + width / 2, center_y + height / 2) - ctx.close_path() + if isinstance(ctx, plt.Axes): + vertices = [ + [center_x - 0.5 * width, center_y - 0.333 * height], + [center_x + 0.5 * width, center_y - 0.333 * height], + [center_x, center_x + 0.667 * height], + ] + return mpl.patches.Polygon(vertices, closed=True, **kwargs) + else: + ctx.move_to(center_x - width / 2, center_y + height / 2) + ctx.line_to(center_x, center_y - height / 2) + ctx.line_to(center_x + width / 2, center_y + height / 2) + ctx.close_path() @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): @@ -201,19 +219,28 @@ def intersection_point(center_x, center_y, source_x, source_y, width, height=Non class DownTriangleDrawer(ShapeDrawer): """Static class which draws triangles pointing down""" - names = "down-triangle triangle-down arrow-down down-arrow" + names = "down-triangle triangle-down arrow-down down-arrow v" @staticmethod - def draw_path(ctx, center_x, center_y, width, height=None): + def draw_path(ctx, center_x, center_y, width, height=None, **kwargs): """Draws a triangle on the Cairo context without stroking or filling it. @see: ShapeDrawer.draw_path""" height = height or width - ctx.move_to(center_x - width / 2, center_y - height / 2) - ctx.line_to(center_x, center_y + height / 2) - ctx.line_to(center_x + width / 2, center_y - height / 2) - ctx.close_path() + if isinstance(ctx, plt.Axes): + vertices = [ + [center_x - 0.5 * width, center_y + 0.333 * height], + [center_x + 0.5 * width, center_y + 0.333 * height], + [center_x, center_y - 0.667 * height], + ] + return mpl.patches.Polygon(vertices, closed=True, **kwargs) + + else: + ctx.move_to(center_x - width / 2, center_y - height / 2) + ctx.line_to(center_x, center_y + height / 2) + ctx.line_to(center_x + width / 2, center_y - height / 2) + ctx.close_path() @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): @@ -230,20 +257,29 @@ def intersection_point(center_x, center_y, source_x, source_y, width, height=Non class DiamondDrawer(ShapeDrawer): """Static class which draws diamonds (i.e. rhombuses)""" - names = "diamond rhombus" + names = "diamond rhombus d" @staticmethod - def draw_path(ctx, center_x, center_y, width, height=None): + def draw_path(ctx, center_x, center_y, width, height=None, **kwargs): """Draws a rhombus on the Cairo context without stroking or filling it. @see: ShapeDrawer.draw_path""" height = height or width - ctx.move_to(center_x - width / 2, center_y) - ctx.line_to(center_x, center_y + height / 2) - ctx.line_to(center_x + width / 2, center_y) - ctx.line_to(center_x, center_y - height / 2) - ctx.close_path() + if isinstance(ctx, plt.Axes): + vertices = [ + [center_x - 0.5 * width, center_y], + [center_x, center_y - 0.5 * height], + [center_x + 0.5 * width, center_y], + [center_x, center_y + 0.5 * height], + ] + return mpl.patches.Polygon(vertices, closed=True, **kwargs) + else: + ctx.move_to(center_x - width / 2, center_y) + ctx.line_to(center_x, center_y + height / 2) + ctx.line_to(center_x + width / 2, center_y) + ctx.line_to(center_x, center_y - height / 2) + ctx.close_path() @staticmethod def intersection_point(center_x, center_y, source_x, source_y, width, height=None): @@ -276,102 +312,6 @@ def intersection_point(center_x, center_y, source_x, source_y, width, height=Non ##################################################################### -class PolygonDrawer(AbstractCairoDrawer): - """Class that is used to draw polygons. - - The corner points of the polygon can be set by the C{points} - property of the drawer, or passed at construction time. Most - drawing methods in this class also have an extra C{points} - argument that can be used to override the set of points in the - C{points} property.""" - - def __init__(self, context, bbox=(1, 1), points=[]): - """Constructs a new polygon drawer that draws on the given - Cairo context. - - @param context: the Cairo context to draw on - @param bbox: ignored, leave it at its default value - @param points: the list of corner points - """ - super().__init__(context, bbox) - self.points = points - - def draw_path(self, points=None, corner_radius=0): - """Sets up a Cairo path for the outline of a polygon on the given - Cairo context. - - @param points: the coordinates of the corners of the polygon, - in clockwise or counter-clockwise order, or C{None} if we are - about to use the C{points} property of the class. - @param corner_radius: if zero, an ordinary polygon will be drawn. - If positive, the corners of the polygon will be rounded with - the given radius. - """ - if points is None: - points = self.points - - self.context.new_path() - - if len(points) < 2: - # Well, a polygon must have at least two corner points - return - - ctx = self.context - if corner_radius <= 0: - # No rounded corners, this is simple - ctx.move_to(*points[-1]) - for point in points: - ctx.line_to(*point) - return - - # Rounded corners. First, we will take each side of the - # polygon and find what the corner radius should be on - # each corner. If the side is longer than 2r (where r is - # equal to corner_radius), the radius allowed by that side - # is r; if the side is shorter, the radius is the length - # of the side / 2. For each corner, the final corner radius - # is the smaller of the radii on the two sides adjacent to - # the corner. - points = [Point(*point) for point in points] - side_vecs = [v - u for u, v in consecutive_pairs(points, circular=True)] - half_side_lengths = [side.length() / 2 for side in side_vecs] - corner_radii = [corner_radius] * len(points) - for idx in range(len(corner_radii)): - prev_idx = -1 if idx == 0 else idx - 1 - radii = [corner_radius, half_side_lengths[prev_idx], half_side_lengths[idx]] - corner_radii[idx] = min(radii) - - # Okay, move to the last corner, adjusted by corner_radii[-1] - # towards the first corner - ctx.move_to(*(points[-1].towards(points[0], corner_radii[-1]))) - # Now, for each point in points, draw a line towards the - # corner, stopping before it in a distance of corner_radii[idx], - # then draw the corner - u = points[-1] - for idx, (v, w) in enumerate(consecutive_pairs(points, True)): - radius = corner_radii[idx] - ctx.line_to(*v.towards(u, radius)) - aux1 = v.towards(u, radius / 2) - aux2 = v.towards(w, radius / 2) - ctx.curve_to( - aux1.x, aux1.y, aux2.x, aux2.y, *v.towards(w, corner_radii[idx]) - ) - u = v - - def draw(self, points=None): - """Draws the polygon using the current stroke of the Cairo context. - - @param points: the coordinates of the corners of the polygon, - in clockwise or counter-clockwise order, or C{None} if we are - about to use the C{points} property of the class. - """ - self.draw_path(points) - self.context.stroke() - - -##################################################################### - - class ShapeDrawerDirectory: """Static class that resolves shape names to their corresponding shape drawer classes. diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index eb248e5e7..df48bbdb9 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -2,369 +2,18 @@ Drawers for labels on plots. """ -import re +from enum import Enum -from igraph.drawing.baseclasses import AbstractCairoDrawer -from warnings import warn - -__all__ = ("TextAlignment", "TextDrawer") - -__docformat__ = "restructuredtext en" +__all__ = ("TextAlignment", ) ##################################################################### -class TextAlignment: +class TextAlignment(Enum): """Text alignment constants.""" - LEFT, CENTER, RIGHT = "left", "center", "right" - TOP, BOTTOM = "top", "bottom" - - -##################################################################### - - -class TextDrawer(AbstractCairoDrawer): - """Class that draws text on a Cairo context. - - This class supports multi-line text unlike the original Cairo text - drawing methods.""" - - LEFT, CENTER, RIGHT = "left", "center", "right" - TOP, BOTTOM = "top", "bottom" - - def __init__(self, context, text="", halign="center", valign="center"): - """Constructs a new instance that will draw the given `text` on - the given Cairo `context`.""" - super().__init__(context, (0, 0)) - self.text = text - self.halign = halign - self.valign = valign - - def draw(self, wrap=False): - """Draws the text in the current bounding box of the drawer. - - Since the class itself is an instance of `AbstractCairoDrawer`, it - has an attribute named ``bbox`` which will be used as a bounding - box. - - @param wrap: whether to allow re-wrapping of the text if it does not - fit within the bounding box horizontally. - """ - ctx = self.context - bbox = self.bbox - - text_layout = self.get_text_layout(bbox.left, bbox.top, bbox.width, wrap) - if not text_layout: - return - - _, font_descent, line_height = ctx.font_extents()[:3] - yb = ctx.text_extents(text_layout[0][2])[1] - total_height = len(text_layout) * line_height - - if self.valign == self.BOTTOM: - # Bottom vertical alignment - dy = bbox.height - total_height - yb + font_descent - elif self.valign == self.CENTER: - # Centered vertical alignment - dy = (bbox.height - total_height - yb + font_descent + line_height) / 2.0 - else: - # Top vertical alignment - dy = line_height - - for ref_x, ref_y, line in text_layout: - ctx.move_to(ref_x, ref_y + dy) - ctx.show_text(line) - ctx.new_path() - - def get_text_layout(self, x=None, y=None, width=None, wrap=False): - """Calculates the layout of the current text. `x` and `y` denote the - coordinates where the drawing should start. If they are both ``None``, - the current position of the context will be used. - - Vertical alignment settings are not taken into account in this method - as the text is not drawn within a box. - - @param x: The X coordinate of the reference point where the layout should - start. - @param y: The Y coordinate of the reference point where the layout should - start. - @param width: The width of the box in which the text will be fitted. It - matters only when the text is right-aligned or centered. The text - will overflow the box if any of the lines is longer than the box - width and `wrap` is ``False``. - @param wrap: whether to allow re-wrapping of the text if it does not - fit within the given width. - - @return: a list consisting of ``(x, y, line)`` tuples where ``x`` and - ``y`` refer to reference points on the Cairo canvas and ``line`` - refers to the corresponding text that should be plotted there. - """ - ctx = self.context - - if x is None or y is None: - x, y = ctx.get_current_point() - - line_height = ctx.font_extents()[2] - - if wrap: - if width and width > 0: - iterlines = self._iterlines_wrapped(width) - else: - warn("ignoring wrap=True as no width was specified") - else: - iterlines = self._iterlines() - - result = [] - - if self.halign == self.CENTER: - # Centered alignment - if width is None: - width = self.text_extents()[2] - for line, line_width, x_bearing in iterlines: - result.append((x + (width - line_width) / 2.0 - x_bearing, y, line)) - y += line_height - - elif self.halign == self.RIGHT: - # Right alignment - if width is None: - width = self.text_extents()[2] - x += width - for line, line_width, x_bearing in iterlines: - result.append((x - line_width - x_bearing, y, line)) - y += line_height - - else: - # Left alignment - for line, _, x_bearing in iterlines: - result.append((x - x_bearing, y, line)) - y += line_height - - return result - - def draw_at(self, x=None, y=None, width=None, wrap=False): - """Draws the text by setting up an appropriate path on the Cairo - context and filling it. `x` and `y` denote the coordinates where the - drawing should start. If they are both ``None``, the current position - of the context will be used. - - Vertical alignment settings are not taken into account in this method - as the text is not drawn within a box. - - @param x: The X coordinate of the reference point where the layout should - start. - @param y: The Y coordinate of the reference point where the layout should - start. - @param width: The width of the box in which the text will be fitted. It - matters only when the text is right-aligned or centered. The text - will overflow the box if any of the lines is longer than the box - width and `wrap` is ``False``. - @param wrap: whether to allow re-wrapping of the text if it does not - fit within the given width. - """ - ctx = self.context - for ref_x, ref_y, line in self.get_text_layout(x, y, width, wrap): - ctx.move_to(ref_x, ref_y) - ctx.show_text(line) - ctx.new_path() - - def _iterlines(self): - """Iterates over the label line by line and returns a tuple containing - the folloing for each line: the line itself, the width of the line and - the X-bearing of the line.""" - ctx = self.context - for line in self._text.split("\n"): - xb, _, line_width, _, _, _ = ctx.text_extents(line) - yield (line, line_width, xb) - - def _iterlines_wrapped(self, width): - """Iterates over the label line by line and returns a tuple containing - the folloing for each line: the line itself, the width of the line and - the X-bearing of the line. - - The difference between this method and `_iterlines()` is that this - method is allowed to re-wrap the line if necessary. - - @param width: The width of the box in which the text will be fitted. - Lines will be wrapped if they are wider than this width. - """ - ctx = self.context - for line in self._text.split("\n"): - xb, _, line_width, _, _, _ = ctx.text_extents(line) - if line_width <= width: - yield (line, line_width, xb) - continue - - # We have to wrap the line - current_line, current_width, last_sep_width = [], 0, 0 - for match in re.finditer(r"(\S+)(\s+)?", line): - word, sep = match.groups() - word_width = ctx.text_extents(word)[4] - if sep: - sep_width = ctx.text_extents(sep)[4] - else: - sep_width = 0 - current_width += word_width - if current_width >= width and current_line: - yield ("".join(current_line), current_width - word_width, 0) - # Starting a new line - current_line, current_width = [word], word_width - if sep is not None: - current_line.append(sep) - else: - current_width += last_sep_width - current_line.append(word) - if sep is not None: - current_line.append(sep) - last_sep_width = sep_width - if current_line: - yield ("".join(current_line), current_width, 0) - - @property - def text(self): - """Returns the text to be drawn.""" - return self._text - - @text.setter - def text(self, text): - """Sets the text that will be drawn. - - If `text` is ``None``, it will be mapped to an empty string; otherwise, - it will be converted to a string.""" - if text is None: - self._text = "" - else: - self._text = str(text) - - def text_extents(self): - """Returns the X-bearing, Y-bearing, width, height, X-advance and - Y-advance of the text. - - For multi-line text, the X-bearing and Y-bearing correspond to the - first line, while the X-advance is extracted from the last line. - and the Y-advance is the sum of all the Y-advances. The width and - height correspond to the entire bounding box of the text.""" - lines = self.text.split("\n") - if len(lines) <= 1: - return self.context.text_extents(self.text) - - ( - x_bearing, - y_bearing, - width, - height, - x_advance, - y_advance, - ) = self.context.text_extents(lines[0]) - - line_height = self.context.font_extents()[2] - for line in lines[1:]: - _, _, w, _, x_advance, ya = self.context.text_extents(line) - width = max(width, w) - height += line_height - y_advance += ya - - return x_bearing, y_bearing, width, height, x_advance, y_advance - - -def test(): - """Testing routine for L{TextDrawer}""" - import math - from igraph.drawing.utils import find_cairo - - cairo = find_cairo() - - text = "The quick brown fox\njumps over a\nlazy dog" - width, height = (600, 1000) - - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - context = cairo.Context(surface) - drawer = TextDrawer(context, text) - - context.set_source_rgb(1, 1, 1) - context.set_font_size(16.0) - context.rectangle(0, 0, width, height) - context.fill() - - context.set_source_rgb(0.5, 0.5, 0.5) - for i in range(200, width, 200): - context.move_to(i, 0) - context.line_to(i, height) - context.stroke() - for i in range(200, height, 200): - context.move_to(0, i) - context.line_to(width, i) - context.stroke() - context.set_source_rgb(0.75, 0.75, 0.75) - context.set_line_width(0.5) - for i in range(100, width, 200): - context.move_to(i, 0) - context.line_to(i, height) - context.stroke() - for i in range(100, height, 200): - context.move_to(0, i) - context.line_to(width, i) - context.stroke() - - def mark_point(red, green, blue): - """Marks the current point on the canvas by the given color""" - x, y = context.get_current_point() - context.set_source_rgba(red, green, blue, 0.5) - context.arc(x, y, 4, 0, 2 * math.pi) - context.fill() - - # Testing drawer.draw_at() - for i, halign in enumerate(("left", "center", "right")): - # Mark the reference points - context.move_to(i * 200, 40) - mark_point(0, 0, 1) - context.move_to(i * 200, 140) - mark_point(0, 0, 1) - - # Draw the text - context.set_source_rgb(0, 0, 0) - drawer.halign = halign - drawer.draw_at(i * 200, 40) - drawer.draw_at(i * 200, 140, width=200) - - # Mark the new reference point - mark_point(1, 0, 0) - - # Testing TextDrawer.draw() - for i, halign in enumerate(("left", "center", "right")): - for j, valign in enumerate(("top", "center", "bottom")): - # Draw the text - context.set_source_rgb(0, 0, 0) - drawer.halign = halign - drawer.valign = valign - drawer.bbox = (i * 200, j * 200 + 200, i * 200 + 200, j * 200 + 400) - drawer.draw() - # Mark the new reference point - mark_point(1, 0, 0) - - # Testing TextDrawer.wrap() - drawer.text = ( - "Jackdaws love my big sphinx of quartz. Yay, wrapping! " - + "Jackdaws love my big sphinx of quartz.\n\n" - + "Jackdaws love my big sphinx of quartz." - ) - drawer.valign = TextDrawer.TOP - for i, halign in enumerate(("left", "center", "right")): - context.move_to(i * 200, 840) - - # Mark the reference point - mark_point(0, 0, 1) - - # Draw the text - context.set_source_rgb(0, 0, 0) - drawer.halign = halign - drawer.draw_at(i * 200, 840, width=199, wrap=True) - - # Mark the new reference point - mark_point(1, 0, 0) - - surface.write_to_png("test.png") - - -if __name__ == "__main__": - test() + LEFT = "left" + CENTER = "center" + RIGHT = "right" + TOP = "top" + BOTTOM = "bottom" diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index ecf546320..216d13298 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -2,11 +2,21 @@ Utility classes for drawing routines. """ - -from math import atan2, cos, sin -from operator import itemgetter - -__all__ = ("BoundingBox", "Point", "Rectangle") +from math import atan2, cos, hypot, sin +from typing import NamedTuple + +from igraph.utils import consecutive_pairs + +__all__ = ( + "BoundingBox", + "Point", + "Rectangle", + "calculate_corner_radii", + "euclidean_distance", + "evaluate_cubic_bezier", + "intersect_bezier_curve_and_circle", + "str_to_orientation", +) ##################################################################### @@ -400,6 +410,7 @@ def __or__(self, other): ##################################################################### + class FakeModule: """Fake module that raises an exception for everything""" @@ -426,91 +437,9 @@ def __setattr__(self, key, value): ##################################################################### -def find_cairo(): - """Tries to import the ``cairo`` Python module if it is installed, - also trying ``cairocffi`` (a drop-in replacement of ``cairo``). - Returns a fake module if everything fails. - """ - module_names = ["cairo", "cairocffi"] - module = FakeModule("Plotting not available; please install pycairo or cairocffi") - for module_name in module_names: - try: - module = __import__(module_name) - break - except ImportError: - pass - return module - - -##################################################################### - - -def find_matplotlib(): - """Tries to import the ``matplotlib`` Python module if it is installed. - Returns a fake module if everything fails. - """ - try: - import matplotlib as mpl - - has_mpl = True - except ImportError: - mpl = FakeModule("You need to install matplotlib to use this functionality") - has_mpl = False - - if has_mpl: - import matplotlib.pyplot as plt - else: - plt = FakeModule( - "You need to install matplotlib.pyplot to use this functionality" - ) - - return mpl, plt - - -##################################################################### - - -class Point(tuple): +class Point(NamedTuple("_Point", [("x", float), ("y", float)])): """Class representing a point on the 2D plane.""" - __slots__ = () - _fields = ("x", "y") - - def __new__(cls, x, y): - """Creates a new point with the given coordinates""" - return tuple.__new__(cls, (x, y)) - - @classmethod - def _make(cls, iterable, new=tuple.__new__, len=len): - """Creates a new point from a sequence or iterable""" - result = new(cls, iterable) - if len(result) != 2: - raise TypeError("Expected 2 arguments, got %d" % len(result)) - return result - - def __repr__(self): - """Returns a nicely formatted representation of the point""" - return "Point(x=%r, y=%r)" % self - - def _asdict(self): - """Returns a new dict which maps field names to their values""" - return dict(zip(self._fields, self)) - - def _replace(self, **kwds): - """Returns a new point object replacing specified fields with new - values""" - result = self._make(map(kwds.pop, ("x", "y"), self)) - if kwds: - raise ValueError("Got unexpected field names: %r" % list(kwds.keys())) - return result - - def __getnewargs__(self): - """Return self as a plain tuple. Used by copy and pickle.""" - return tuple(self) - - x = property(itemgetter(0), doc="Alias for field number 0") - y = property(itemgetter(1), doc="Alias for field number 1") - def __add__(self, other): """Adds the coordinates of a point to another one""" return self.__class__(x=self.x + other.x, y=self.y + other.y) @@ -558,7 +487,7 @@ def interpolate(self, other, ratio=0.5): return this point, 1 will return the other point. """ ratio = float(ratio) - return Point( + return self.__class__( x=self.x * (1.0 - ratio) + other.x * ratio, y=self.y * (1.0 - ratio) + other.y * ratio, ) @@ -573,8 +502,8 @@ def normalized(self): after normalization. Returns the normalized point.""" len = self.length() if len == 0: - return Point(x=self.x, y=self.y) - return Point(x=self.x / len, y=self.y / len) + return self.__class__(x=self.x, y=self.y) + return self.__class__(x=self.x / len, y=self.y / len) def sq_length(self): """Returns the squared length of the vector pointing from the origin @@ -588,7 +517,9 @@ def towards(self, other, distance=0): return self angle = atan2(other.y - self.y, other.x - self.x) - return Point(self.x + distance * cos(angle), self.y + distance * sin(angle)) + return self.__class__( + self.x + distance * cos(angle), self.y + distance * sin(angle) + ) @classmethod def FromPolar(cls, radius, angle): @@ -599,3 +530,138 @@ def FromPolar(cls, radius, angle): the origin. """ return cls(radius * cos(angle), radius * sin(angle)) + + +def calculate_corner_radii(points, corner_radius): + """Given a list of points and a desired corner radius, returns a list + containing proposed corner radii for each of the points such that it is + ensured that the corner radius at a point is never larger than half of + the minimum distance between the point and its neighbors. + """ + points = [Point(*point) for point in points] + side_vecs = [v - u for u, v in consecutive_pairs(points, circular=True)] + half_side_lengths = [side.length() / 2 for side in side_vecs] + corner_radii = [corner_radius] * len(points) + for idx in range(len(corner_radii)): + prev_idx = -1 if idx == 0 else idx - 1 + radii = [corner_radius, half_side_lengths[prev_idx], half_side_lengths[idx]] + corner_radii[idx] = min(radii) + return corner_radii + + +def euclidean_distance(x1, y1, x2, y2): + """Computes the Euclidean distance between points (x1,y1) and (x2,y2).""" + return hypot(x2 - x1, y2 - y1) + + +def evaluate_cubic_bezier(x0, y0, x1, y1, x2, y2, x3, y3, t): + """Evaluates the Bezier curve from point (x0,y0) to (x3,y3) + via control points (x1,y1) and (x2,y2) at t. t is typically in the range + [0; 1] such that 0 returns (x0, y0) and 1 returns (x3, y3). + """ + xt = ( + (1.0 - t) ** 3 * x0 + + 3.0 * t * (1.0 - t) ** 2 * x1 + + 3.0 * t ** 2 * (1.0 - t) * x2 + + t ** 3 * x3 + ) + yt = ( + (1.0 - t) ** 3 * y0 + + 3.0 * t * (1.0 - t) ** 2 * y1 + + 3.0 * t ** 2 * (1.0 - t) * y2 + + t ** 3 * y3 + ) + return xt, yt + + +def intersect_bezier_curve_and_circle( + x0, y0, x1, y1, x2, y2, x3, y3, radius, max_iter=10 +): + """Binary search solver for finding the intersection of a Bezier curve + and a circle centered at the curve's end point. + + Returns the x, y coordinates of the intersection point. + """ + # The exact formulation of the problem is a quartic equation and it is + # probably not worth coding up an exact quartic solver. The solution below + # uses binary search. Another solution would be simply to intersect the + # circle with the line pointing from (x2, y2) to (x3, y3) as the difference + # is likely to be negligible. + + precision = radius / 20.0 + source_target_distance = euclidean_distance(x0, y0, x3, y3) + radius = float(radius) + t0 = 1.0 + t1 = 1.0 - radius / source_target_distance + + xt1, yt1 = evaluate_cubic_bezier(x0, y0, x1, y1, x2, y2, x3, y3, t1) + + distance_t0 = 0 + distance_t1 = euclidean_distance(x3, y3, xt1, yt1) + counter = 0 + while abs(distance_t1 - radius) > precision and counter < max_iter: + if ((distance_t1 - radius) > 0) != ((distance_t0 - radius) > 0): + t_new = (t0 + t1) / 2.0 + else: + if abs(distance_t1 - radius) < abs(distance_t0 - radius): + # If t1 gets us closer to the circumference step in the + # same direction + t_new = t1 + (t1 - t0) / 2.0 + else: + t_new = t1 - (t1 - t0) + t_new = 1 if t_new > 1 else (0 if t_new < 0 else t_new) + t0, t1 = t1, t_new + distance_t0 = distance_t1 + xt1, yt1 = evaluate_cubic_bezier(x0, y0, x1, y1, x2, y2, x3, y3, t1) + distance_t1 = euclidean_distance(x3, y3, xt1, yt1) + counter += 1 + + return evaluate_cubic_bezier(x0, y0, x1, y1, x2, y2, x3, y3, t1) + + +def str_to_orientation(value, reversed_horizontal=False, reversed_vertical=False): + """Tries to interpret a string as an orientation value. + + The following basic values are understood: ``left-right``, ``bottom-top``, + ``right-left``, ``top-bottom``. Possible aliases are: + + - ``horizontal``, ``horiz``, ``h`` and ``lr`` for ``left-right`` + + - ``vertical``, ``vert``, ``v`` and ``tb`` for top-bottom. + + - ``lr`` for ``left-right``. + + - ``rl`` for ``right-left``. + + ``reversed_horizontal`` reverses the meaning of ``horizontal``, ``horiz`` + and ``h`` to ``rl`` (instead of ``lr``); similarly, ``reversed_vertical`` + reverses the meaning of ``vertical``, ``vert`` and ``v`` to ``bt`` + (instead of ``tb``). + + Returns one of ``lr``, ``rl``, ``tb`` or ``bt``, or throws ``ValueError`` + if the string cannot be interpreted as an orientation. + """ + + aliases = { + "left-right": "lr", + "right-left": "rl", + "top-bottom": "tb", + "bottom-top": "bt", + "top-down": "tb", + "bottom-up": "bt", + "top-bottom": "tb", + "bottom-top": "bt", + "td": "tb", + "bu": "bt", + } + + dir = ["lr", "rl"][reversed_horizontal] + aliases.update(horizontal=dir, horiz=dir, h=dir) + + dir = ["tb", "bt"][reversed_vertical] + aliases.update(vertical=dir, vert=dir, v=dir) + + result = aliases.get(value, value) + if result not in ("lr", "rl", "tb", "bt"): + raise ValueError("unknown orientation: %s" % result) + return result diff --git a/src/igraph/formula.py b/src/igraph/formula.py index ad5473d15..6ea24ce21 100644 --- a/src/igraph/formula.py +++ b/src/igraph/formula.py @@ -75,8 +75,7 @@ def generate_edges(formula): else: msg = ( "invalid token found in edge specification: %s; " - "token_type=%r; tok=%r" - % (formula, token_type, tok) + "token_type=%r; tok=%r" % (formula, token_type, tok) ) raise SyntaxError(msg) else: diff --git a/src/igraph/statistics.py b/src/igraph/statistics.py index 68350b86a..73773aec3 100644 --- a/src/igraph/statistics.py +++ b/src/igraph/statistics.py @@ -230,39 +230,12 @@ def bins(self): yield (x, x + self._bin_width, elem) x += self._bin_width - def __plot__(self, context, bbox, _, **kwds): + def __plot__(self, backend, context, **kwds): """Plotting support""" - from igraph.drawing.coord import DescartesCoordinateSystem - - coord_system = DescartesCoordinateSystem( - context, - bbox, - ( - kwds.get("min", self._min), - 0, - kwds.get("max", self._max), - kwds.get("max_value", max(self._bins)), - ), - ) - - # Draw the boxes - context.set_line_width(1) - context.set_source_rgb(1.0, 0.0, 0.0) - x = self._min - for value in self._bins: - top_left_x, top_left_y = coord_system.local_to_context(x, value) - x += self._bin_width - bottom_right_x, bottom_right_y = coord_system.local_to_context(x, 0) - context.rectangle( - top_left_x, - top_left_y, - bottom_right_x - top_left_x, - bottom_right_y - top_left_y, - ) - context.fill() + from igraph.drawing import DrawerDirectory - # Draw the axes - coord_system.draw() + drawer = DrawerDirectory.resolve(self, backend)(context) + drawer.draw(self, **kwds) def to_string(self, max_width=78, show_bars=True, show_counts=True): """Returns the string representation of the histogram. diff --git a/src/igraph/utils.py b/src/igraph/utils.py index d195a6263..f919ef717 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -142,54 +142,6 @@ def rescale(values, out_range=(0.0, 1.0), in_range=None, clamp=False, scale=None return result -def str_to_orientation(value, reversed_horizontal=False, reversed_vertical=False): - """Tries to interpret a string as an orientation value. - - The following basic values are understood: ``left-right``, ``bottom-top``, - ``right-left``, ``top-bottom``. Possible aliases are: - - - ``horizontal``, ``horiz``, ``h`` and ``lr`` for ``left-right`` - - - ``vertical``, ``vert``, ``v`` and ``tb`` for top-bottom. - - - ``lr`` for ``left-right``. - - - ``rl`` for ``right-left``. - - ``reversed_horizontal`` reverses the meaning of ``horizontal``, ``horiz`` - and ``h`` to ``rl`` (instead of ``lr``); similarly, ``reversed_vertical`` - reverses the meaning of ``vertical``, ``vert`` and ``v`` to ``bt`` - (instead of ``tb``). - - Returns one of ``lr``, ``rl``, ``tb`` or ``bt``, or throws ``ValueError`` - if the string cannot be interpreted as an orientation. - """ - - aliases = { - "left-right": "lr", - "right-left": "rl", - "top-bottom": "tb", - "bottom-top": "bt", - "top-down": "tb", - "bottom-up": "bt", - "top-bottom": "tb", - "bottom-top": "bt", - "td": "tb", - "bu": "bt", - } - - dir = ["lr", "rl"][reversed_horizontal] - aliases.update(horizontal=dir, horiz=dir, h=dir) - - dir = ["tb", "bt"][reversed_vertical] - aliases.update(vertical=dir, vert=dir, v=dir) - - result = aliases.get(value, value) - if result not in ("lr", "rl", "tb", "bt"): - raise ValueError("unknown orientation: %s" % result) - return result - - def consecutive_pairs(iterable, circular=False): """Returns consecutive pairs of items from the given iterable. diff --git a/tests/drawing/__init__.py b/tests/drawing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/drawing/cairo/__init__.py b/tests/drawing/cairo/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/drawing/cairo/baseline_images/clustering_directed.png b/tests/drawing/cairo/baseline_images/clustering_directed.png new file mode 100644 index 0000000000000000000000000000000000000000..32baed2ca0a88cf68d3f8283a6b951b1fd0613b1 GIT binary patch literal 30616 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 l+ww`H3IY{DQ8_Iiu=#kWLfG}!dEvmnH literal 0 HcmV?d00001 diff --git a/tests/drawing/cairo/baseline_images/clustering_directed_large.png b/tests/drawing/cairo/baseline_images/clustering_directed_large.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6b54aa62cfba120ae0694cdd8c977fcd374e20 GIT binary patch literal 64832 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
+ API Documentation for Some +- Project, generated by pydoctor at some time. ++ Project, generated by pydoctor . +
+ + +diff --git a/pydoctor/templatewriter/summary.py b/pydoctor/templatewriter/summary.py +index e5f4050..3d326b3 100644 +--- a/pydoctor/templatewriter/summary.py ++++ b/pydoctor/templatewriter/summary.py +@@ -22,7 +22,7 @@ + return r + ul = tags.ul() + for m in sorted(contents, key=lambda m:m.fullName()): +- ul(moduleSummary(m, page_url)) ++ ul(moduleSummary(m, page_url), "\n") + return r(ul) + + def _lckey(x): +@@ -45,6 +45,7 @@ + r = [] + for o in self.system.rootobjects: + r.append(moduleSummary(o, self.filename)) ++ r.append("\n") + return tag.clear()(r) + @renderer + def heading(self, request, tag): +@@ -105,7 +106,7 @@ + if len(scs) > 0: + ul = tags.ul() + for sc in sorted(scs, key=_lckey): +- ul(subclassesFrom(hostsystem, sc, anchors, page_url)) ++ ul(subclassesFrom(hostsystem, sc, anchors, page_url), "\n") + r(ul) + return r + +@@ -136,9 +137,10 @@ + if o: + ul = tags.ul() + for sc in sorted(o, key=_lckey): +- ul(subclassesFrom(self.system, sc, anchors, self.filename)) ++ ul(subclassesFrom(self.system, sc, anchors, self.filename), "\n") + item(ul) + t(item) ++ t("\n") + return t + + @renderer From 8587c9ada4ce461d70d3c73615450393da312ec1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 18:32:38 +0100 Subject: [PATCH 0489/1681] fix: fix Read_GraphMLz function by removing 'directed' argument, closes #454 --- src/_igraph/graphobject.c | 2 +- src/igraph/__init__.py | 4 +- tests/test_foreign.py | 108 ++++++++++++++++++++++---------------- 3 files changed, 65 insertions(+), 49 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 4b797c98f..388064a8c 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15172,7 +15172,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_graphml */ {"Read_GraphML", (PyCFunction) igraphmodule_Graph_Read_GraphML, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_GraphML(f, directed=True, index=0)\n--\n\n" + "Read_GraphML(f, index=0)\n--\n\n" "Reads a GraphML format file and creates a graph based on it.\n\n" "@param f: the name of the file or a Python file handle\n" "@param index: if the GraphML file contains multiple graphs,\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 65f44c4ee..7f616ef1f 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2300,7 +2300,7 @@ def Read_DIMACS(cls, f, directed=False): return graph @classmethod - def Read_GraphMLz(cls, f, directed=True, index=0): + def Read_GraphMLz(cls, f, index=0): """Reads a graph from a zipped GraphML file. @param f: the name of the file @@ -2312,7 +2312,7 @@ def Read_GraphMLz(cls, f, directed=True, index=0): with named_temporary_file() as tmpfile: with open(tmpfile, "wb") as outf: copyfileobj(gzip.GzipFile(f, "rb"), outf) - return cls.Read_GraphML(tmpfile, directed=directed, index=index) + return cls.Read_GraphML(tmpfile, index=index) def write_pickle(self, fname=None, version=-1): """Saves the graph in Python pickled format diff --git a/tests/test_foreign.py b/tests/test_foreign.py index df45281bb..cb0cf3e60 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -1,3 +1,4 @@ +import gzip import io import unittest import warnings @@ -22,6 +23,51 @@ pd = None +GRAPHML_EXAMPLE_FILE = """\ + + + + + + + a + + + b + + + c + + + d + + + e + + + f + + + + + + + + + + + + + + + + + +""" + class ForeignTests(unittest.TestCase): def testDIMACS(self): with temporary_file( @@ -302,52 +348,7 @@ def testAdjacency(self): g.write_adjacency(tmpfname) def testGraphML(self): - with temporary_file( - """\ - - - - - - - a - - - b - - - c - - - d - - - e - - - f - - - - - - - - - - - - - - - - - - """ - ) as tmpfname: + with temporary_file(GRAPHML_EXAMPLE_FILE) as tmpfname: try: g = Graph.Read_GraphML(tmpfname) except NotImplementedError as e: @@ -361,6 +362,21 @@ def testGraphML(self): g.write_graphml(tmpfname) + def testGraphMLz(self): + with temporary_file(gzip.compress(GRAPHML_EXAMPLE_FILE.encode("utf-8"))) as tmpfname: + try: + g = Graph.Read_GraphMLz(tmpfname) + except NotImplementedError as e: + self.skipTest(str(e)) + + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(g.vcount(), 6) + self.assertEqual(g.ecount(), 7) + self.assertFalse(g.is_directed()) + self.assertTrue("name" in g.vertex_attributes()) + + g.write_graphmlz(tmpfname) + def testPickle(self): pickle = [ 128, From 6fc5a29a18da6516b90f1ed46d425638ea513af9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 18:54:37 +0100 Subject: [PATCH 0490/1681] fix: fix failing test case on Windows x64 --- tests/test_foreign.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_foreign.py b/tests/test_foreign.py index cb0cf3e60..6359ccb7b 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -375,8 +375,6 @@ def testGraphMLz(self): self.assertFalse(g.is_directed()) self.assertTrue("name" in g.vertex_attributes()) - g.write_graphmlz(tmpfname) - def testPickle(self): pickle = [ 128, From 46ed90e518028653a73c93c5a527ac4e90f9368f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 19:23:15 +0100 Subject: [PATCH 0491/1681] fix: fix failing test case on Windows x64, hopefully it works this time --- tests/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index fdea66dba..c4158416b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -30,7 +30,13 @@ def temporary_file(content=None, mode=None, binary=False): tmpf.close() yield tmpfname - os.unlink(tmpfname) + try: + os.unlink(tmpfname) + except Exception: + # ignore exceptions; it happens sometimes on Windows in the CI environment + # that it cannot remove the temporary file because another process (?) is + # using it... + pass is_pypy = platform.python_implementation() == "PyPy" From e72c4ca609181a507a9a98096b4e435184eae5e5 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 19:32:56 +0100 Subject: [PATCH 0492/1681] chore: reformatted test.sh --- test.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test.sh b/test.sh index b3e124265..7604b5582 100755 --- a/test.sh +++ b/test.sh @@ -17,11 +17,11 @@ while getopts ":ce:k:" OPTION; do CLEAN=1 ;; e) - VENV_DIR=$OPTARG + VENV_DIR=$OPTARG + ;; + k) + PYTEST_ARGS="${PYTEST_ARGS} -k $OPTARG" ;; - k) - PYTEST_ARGS="${PYTEST_ARGS} -k $OPTARG" - ;; \?) echo "Usage: $0 [-c]" ;; From d1e86d573cb001362d24b1d88fc702ee74cd6c17 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 18:32:38 +0100 Subject: [PATCH 0493/1681] fix: fix Read_GraphMLz function by removing 'directed' argument, closes #454 --- src/_igraph/graphobject.c | 2 +- tests/test_foreign.py | 108 ++++++++++++++++++++++---------------- 2 files changed, 63 insertions(+), 47 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 3f8758d3c..51bddd737 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -15282,7 +15282,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { /* interface to igraph_read_graph_graphml */ {"Read_GraphML", (PyCFunction) igraphmodule_Graph_Read_GraphML, METH_VARARGS | METH_KEYWORDS | METH_CLASS, - "Read_GraphML(f, directed=True, index=0)\n--\n\n" + "Read_GraphML(f, index=0)\n--\n\n" "Reads a GraphML format file and creates a graph based on it.\n\n" "@param f: the name of the file or a Python file handle\n" "@param index: if the GraphML file contains multiple graphs,\n" diff --git a/tests/test_foreign.py b/tests/test_foreign.py index c4c77978d..3640110e5 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -1,3 +1,4 @@ +import gzip import io import unittest import warnings @@ -22,6 +23,51 @@ pd = None +GRAPHML_EXAMPLE_FILE = """\ + + + + + + + a + + + b + + + c + + + d + + + e + + + f + + + + + + + + + + + + + + + + + +""" + class ForeignTests(unittest.TestCase): def testDIMACS(self): with temporary_file( @@ -299,52 +345,7 @@ def testAdjacency(self): g.write_adjacency(tmpfname) def testGraphML(self): - with temporary_file( - """\ - - - - - - - a - - - b - - - c - - - d - - - e - - - f - - - - - - - - - - - - - - - - - - """ - ) as tmpfname: + with temporary_file(GRAPHML_EXAMPLE_FILE) as tmpfname: try: g = Graph.Read_GraphML(tmpfname) except NotImplementedError as e: @@ -358,6 +359,21 @@ def testGraphML(self): g.write_graphml(tmpfname) + def testGraphMLz(self): + with temporary_file(gzip.compress(GRAPHML_EXAMPLE_FILE.encode("utf-8"))) as tmpfname: + try: + g = Graph.Read_GraphMLz(tmpfname) + except NotImplementedError as e: + self.skipTest(str(e)) + + self.assertTrue(isinstance(g, Graph)) + self.assertEqual(g.vcount(), 6) + self.assertEqual(g.ecount(), 7) + self.assertFalse(g.is_directed()) + self.assertTrue("name" in g.vertex_attributes()) + + g.write_graphmlz(tmpfname) + def testPickle(self): pickle = [ 128, From cf8213d9b2a8520e2fbd82ec587c31bb2551c290 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 18:54:37 +0100 Subject: [PATCH 0494/1681] fix: fix failing test case on Windows x64 --- tests/test_foreign.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_foreign.py b/tests/test_foreign.py index 3640110e5..5e117179a 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -372,8 +372,6 @@ def testGraphMLz(self): self.assertFalse(g.is_directed()) self.assertTrue("name" in g.vertex_attributes()) - g.write_graphmlz(tmpfname) - def testPickle(self): pickle = [ 128, From 8d1ae5493007092367c6a30e059afa7ea3fa63fe Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 19:23:15 +0100 Subject: [PATCH 0495/1681] fix: fix failing test case on Windows x64, hopefully it works this time --- tests/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index fdea66dba..c4158416b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -30,7 +30,13 @@ def temporary_file(content=None, mode=None, binary=False): tmpf.close() yield tmpfname - os.unlink(tmpfname) + try: + os.unlink(tmpfname) + except Exception: + # ignore exceptions; it happens sometimes on Windows in the CI environment + # that it cannot remove the temporary file because another process (?) is + # using it... + pass is_pypy = platform.python_implementation() == "PyPy" From c149b16663b6f8a5c1e7eedd690aacbc70244702 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 3 Nov 2021 19:56:41 +0100 Subject: [PATCH 0496/1681] fix: fix incorrect merge --- src/igraph/io/files.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/igraph/io/files.py b/src/igraph/io/files.py index a39ae438f..64c70c4d2 100644 --- a/src/igraph/io/files.py +++ b/src/igraph/io/files.py @@ -174,7 +174,7 @@ def _construct_graph_from_dimacs_file(cls, f, directed=False): return graph -def _construct_graph_from_graphmlz_file(cls, f, directed=True, index=0): +def _construct_graph_from_graphmlz_file(cls, f, index=0): """Reads a graph from a zipped GraphML file. @param f: the name of the file @@ -186,7 +186,7 @@ def _construct_graph_from_graphmlz_file(cls, f, directed=True, index=0): with named_temporary_file() as tmpfile: with open(tmpfile, "wb") as outf: copyfileobj(gzip.GzipFile(f, "rb"), outf) - return cls.Read_GraphML(tmpfile, directed=directed, index=index) + return cls.Read_GraphML(tmpfile, index=index) def _construct_graph_from_pickle_file(cls, fname=None): From 29248d26b16e3bd0b99d2cd04ba4b86599f98340 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 5 Nov 2021 10:26:50 +0100 Subject: [PATCH 0497/1681] fix: replace obsolete DrL layout URL --- doc/source/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 57e972502..86a29a4a4 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -727,7 +727,7 @@ Method name Short name Algorithm description ``circular_3d`` ==================================== =============== ============================================= -.. _Distributed Recursive Layout: http://www.cs.sandia.gov/~smartin/software.html +.. _Distributed Recursive Layout: https://www.osti.gov/doecode/biblio/54626 .. _Large Graph Layout: http://sourceforge.net/projects/lgl/ Layout algorithms can either be called directly or using the common layout method called From 7f3cd5a41e2de3d8a4fe356249db1d0c21b82acc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 9 Nov 2021 10:29:04 +0100 Subject: [PATCH 0498/1681] fix: further tweak the text of an error message to make it clear that node IDs must be integers (floats are not okay, even if they are integral), refs #42 --- src/_igraph/convert.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 80fe02a8d..a33d8f40b 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -2511,7 +2511,7 @@ int igraphmodule_PyObject_to_vid(PyObject *o, igraph_integer_t *vid, igraph_t *g } else return 1; } else { - PyErr_SetString(PyExc_TypeError, "only numbers, strings or igraph.Vertex objects can be converted to vertex IDs"); + PyErr_SetString(PyExc_TypeError, "only non-negative integers, strings or igraph.Vertex objects can be converted to vertex IDs"); return 1; } @@ -2756,7 +2756,7 @@ int igraphmodule_PyObject_to_eid(PyObject *o, igraph_integer_t *eid, igraph_t *g } } else { PyErr_SetString(PyExc_TypeError, - "only numbers, igraph.Edge objects or tuples of vertex IDs can be " + "only non-negative integers, igraph.Edge objects or tuples of vertex IDs can be " "converted to edge IDs"); return 1; } From 6c03726af49639af16a8eede97ad9ebb01c48cc1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 9 Nov 2021 15:36:40 +0100 Subject: [PATCH 0499/1681] chore: upgraded vendored igraph to almost-0.9.5 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index b29e741ea..4459083d8 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit b29e741ea4106b259318d1d1d37404d317b2a185 +Subproject commit 4459083d848baf7da637955e904132600273db09 From 0316fdda50155400df280427b8ae9f5b2ad0f9a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horv=C3=A1t?= Date: Tue, 9 Nov 2021 22:01:27 +0100 Subject: [PATCH 0500/1681] docs: update Reingold-Tilford documentation --- src/_igraph/graphobject.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 388064a8c..0de916250 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -14958,11 +14958,15 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " This parameter also influences how the root vertices are calculated\n" " if they are not given. See the I{root} parameter.\n" "@param root: the index of the root vertex or root vertices.\n" - " if this is a non-empty vector then the supplied vertex IDs are\n" + " If this is a non-empty vector then the supplied vertex IDs are\n" " used as the roots of the trees (or a single tree if the graph is\n" - " connected. If this is C{None} or an empty list, the root vertices\n" - " are automatically calculated based on topological sorting,\n" - " performed with the opposite of the I{mode} argument.\n" + " connected). If this is C{None} or an empty list, the root vertices\n" + " are automatically calculated in such a way so that all other vertices\n" + " would be reachable from them. Currently, automatic root selection\n" + " prefers low eccentricity vertices in small graphs (fewer than 500\n" + " vertices) and high degree vertices in large graphs. This heuristic\n" + " may change in future versions. Specify roots manually for a consistent\n" + " output.\n" "@param rootlevel: this argument is useful when drawing forests which are\n" " not trees. It specifies the level of the root vertices for every tree\n" " in the forest.\n" From d51040e5015019cd8a30c078826af8464c545992 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 10 Nov 2021 09:29:55 +0100 Subject: [PATCH 0501/1681] fix: porting PyDoctor patch over to the master branch so we can test the Dash docset --- scripts/mkdoc.sh | 3 + scripts/patch-pydoctor.sh | 23 +++++ scripts/pydoctor-21.2.2.patch | 167 ++++++++++++++++++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100755 scripts/patch-pydoctor.sh create mode 100644 scripts/pydoctor-21.2.2.patch diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index f5ac87264..ebb340588 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -26,6 +26,9 @@ fi PWD=`pwd` +echo "Patching PyDoctor..." +$SCRIPTS_FOLDER/patch-pydoctor.sh ${ROOT_FOLDER} ${SCRIPTS_FOLDER} + echo "Removing existing documentation..." rm -rf "${DOC_API_FOLDER}/html" "${DOC_API_FOLDER}/pdf" diff --git a/scripts/patch-pydoctor.sh b/scripts/patch-pydoctor.sh new file mode 100755 index 000000000..d6ff5a4f7 --- /dev/null +++ b/scripts/patch-pydoctor.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Patches PyDoctor to make it suitable to build python-igraph's documentation +# until our patches get upstreamed + +if [ "x$1" = x ]; then + echo "Usage: $0 ROOT_FOLDER PATCH_FOLDER" + exit 1 +fi + +set -e + +ROOT_FOLDER=$1 +PATCH_FOLDER=$(realpath $2) +${ROOT_FOLDER}/.venv/bin/pip uninstall -y pydoctor +${ROOT_FOLDER}/.venv/bin/pip install pydoctor==21.2.2 +PYDOCTOR_DIR=`.venv/bin/python -c 'import os,pydoctor;print(os.path.dirname(pydoctor.__file__))'` + +cd "${PYDOCTOR_DIR}" +# patch is confirmed to work with pydoctor 21.2.2 +patch -r deleteme.rej -N -p2 <${PATCH_FOLDER}/pydoctor-21.2.2.patch 2>/dev/null +rm -f deleteme.rej + + diff --git a/scripts/pydoctor-21.2.2.patch b/scripts/pydoctor-21.2.2.patch new file mode 100644 index 000000000..15a23623f --- /dev/null +++ b/scripts/pydoctor-21.2.2.patch @@ -0,0 +1,167 @@ +diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py +index 6b274c9..393ddab 100644 +--- a/pydoctor/astbuilder.py ++++ b/pydoctor/astbuilder.py +@@ -188,7 +188,7 @@ class ModuleVistor(ast.NodeVisitor): + self.newAttr = None + + def visit_Module(self, node: ast.Module) -> None: +- assert self.module.docstring is None ++ # assert self.module.docstring is None + + self.builder.push(self.module, 0) + if len(node.body) > 0 and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str): +diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py +index 1b418ae..841c773 100644 +--- a/pydoctor/epydoc2stan.py ++++ b/pydoctor/epydoc2stan.py +@@ -191,6 +191,11 @@ class _EpydocLinker(DocstringLinker): + target = src.resolveName(identifier) + if target is not None: + return target ++ if isinstance(src, model.Class): ++ for base in src.allbases(): ++ target = base.resolveName(identifier) ++ if target is not None: ++ return target + src = src.parent + + # Walk up the object tree again and see if 'identifier' refers to an +@@ -528,7 +533,7 @@ class FieldHandler: + + def handleUnknownField(self, field: Field) -> None: + name = field.tag +- field.report(f"Unknown field '{name}'" ) ++ # field.report(f"Unknown field '{name}'" ) + self.unknowns[name].append(FieldDesc(name=field.arg, body=field.format())) + + def handle(self, field: Field) -> None: +diff --git a/pydoctor/model.py b/pydoctor/model.py +index d233639..a14a4df 100644 +--- a/pydoctor/model.py ++++ b/pydoctor/model.py +@@ -14,7 +14,7 @@ import platform + import sys + import types + from enum import Enum +-from inspect import Signature ++from inspect import signature, Signature + from optparse import Values + from pathlib import Path + from typing import ( +@@ -514,7 +514,8 @@ class Function(Inheritable): + is_async: bool + annotations: Mapping[str, Optional[ast.expr]] + decorators: Optional[Sequence[ast.expr]] +- signature: Signature ++ signature: Optional[Signature] ++ text_signature: str = "" + + def setup(self) -> None: + super().setup() +@@ -776,15 +777,24 @@ class System: + + def _introspectThing(self, thing: object, parent: Documentable, parentMod: _ModuleT) -> None: + for k, v in thing.__dict__.items(): +- if (isinstance(v, (types.BuiltinFunctionType, types.FunctionType)) ++ # TODO(ntamas): MethodDescriptorType and ClassMethodDescriptorType are Python 3.7 only. ++ if (isinstance(v, (types.BuiltinFunctionType, types.FunctionType, types.MethodDescriptorType, types.ClassMethodDescriptorType)) + # In PyPy 7.3.1, functions from extensions are not + # instances of the above abstract types. +- or v.__class__.__name__ == 'builtin_function_or_method'): ++ or (hasattr(v, "__class__") and v.__class__.__name__ == 'builtin_function_or_method')): + f = self.Function(self, k, parent) + f.parentMod = parentMod + f.docstring = v.__doc__ + f.decorators = None +- f.signature = Signature() ++ try: ++ f.signature = signature(v) ++ except Exception: ++ f.text_signature = (getattr(v, "__text_signature__") or "") + " (INVALID)" ++ f.signature = None ++ f.is_async = False ++ f.annotations = { ++ name: None for name in (f.signature.parameters if f.signature else {}) ++ } + self.addObject(f) + elif isinstance(v, type): + c = self.Class(self, k, parent) +@@ -912,7 +922,8 @@ class System: + mod.state = ProcessingState.PROCESSED + head = self.processing_modules.pop() + assert head == mod.fullName() +- self.unprocessed_modules.remove(mod) ++ if mod in self.unprocessed_modules: ++ self.unprocessed_modules.remove(mod) + self.progress( + 'process', + self.module_count - len(self.unprocessed_modules), +diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py +index e5f4050..3d326b3 100644 +--- a/pydoctor/templatewriter/pages/__init__.py ++++ b/pydoctor/templatewriter/pages/__init__.py +@@ -49,7 +49,7 @@ def format_decorators(obj: Union[model.Function, model.Attribute]) -> Iterator[A + + def signature(function: model.Function) -> str: + """Return a nicely-formatted source-like function signature.""" +- return str(function.signature) ++ return str(function.signature) if function.signature else function.text_signature or "(...)" + + class DocGetter: + """L{epydoc2stan} bridge.""" +diff --git a/pydoctor/templates/common.html b/pydoctor/templates/common.html +index e5f4050..3d326b3 100644 +--- a/pydoctor/templates/common.html ++++ b/pydoctor/templates/common.html +@@ -63,7 +63,7 @@ + +
+ API Documentation for Some +- Project, generated by pydoctor at some time. ++ Project, generated by pydoctor . +
+ + +diff --git a/pydoctor/templatewriter/summary.py b/pydoctor/templatewriter/summary.py +index e5f4050..3d326b3 100644 +--- a/pydoctor/templatewriter/summary.py ++++ b/pydoctor/templatewriter/summary.py +@@ -22,7 +22,7 @@ + return r + ul = tags.ul() + for m in sorted(contents, key=lambda m:m.fullName()): +- ul(moduleSummary(m, page_url)) ++ ul(moduleSummary(m, page_url), "\n") + return r(ul) + + def _lckey(x): +@@ -45,6 +45,7 @@ + r = [] + for o in self.system.rootobjects: + r.append(moduleSummary(o, self.filename)) ++ r.append("\n") + return tag.clear()(r) + @renderer + def heading(self, request, tag): +@@ -105,7 +106,7 @@ + if len(scs) > 0: + ul = tags.ul() + for sc in sorted(scs, key=_lckey): +- ul(subclassesFrom(hostsystem, sc, anchors, page_url)) ++ ul(subclassesFrom(hostsystem, sc, anchors, page_url), "\n") + r(ul) + return r + +@@ -136,9 +137,10 @@ + if o: + ul = tags.ul() + for sc in sorted(o, key=_lckey): +- ul(subclassesFrom(self.system, sc, anchors, self.filename)) ++ ul(subclassesFrom(self.system, sc, anchors, self.filename), "\n") + item(ul) + t(item) ++ t("\n") + return t + + @renderer From 66711348f90cc322da0b03e3104157f31e99c41d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 11 Nov 2021 21:19:50 +0100 Subject: [PATCH 0502/1681] chore: updated vendored igraph to 0.9.5 --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 4459083d8..8bb3224ab 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 4459083d848baf7da637955e904132600273db09 +Subproject commit 8bb3224ab3cfb71bacb0621412d3d43b61a5d676 From c6794ca8ece71fcceb12a1fbd7cf19af9477704b Mon Sep 17 00:00:00 2001 From: Sriram-Pattabiraman <59712515+Sriram-Pattabiraman@users.noreply.github.com> Date: Sat, 13 Nov 2021 11:41:53 -0600 Subject: [PATCH 0503/1681] Changed get_label_position func to account for edge curvature get_label_position now computes the bezier curve for the edge that would be drawn, then finds the parameter based midpoint by putting `.5` as the parameter and taking the output as the label position. --- src/igraph/drawing/edge.py | 40 ++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 605b2df5c..88cc1909e 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -158,11 +158,43 @@ def get_label_position(self, edge, src_vertex, dest_vertex): else: angle = None + + def bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t): + """Computes the Bezier curve from point (x0,y0) to (x3,y3) + via control points (x1,y1) and (x2,y2) with parameter t. + """ + xt = ( + (1.0 - t) ** 3 * x0 + + 3.0 * t * (1.0 - t) ** 2 * x1 + + 3.0 * t ** 2 * (1.0 - t) * x2 + + t ** 3 * x3 + ) + yt = ( + (1.0 - t) ** 3 * y0 + + 3.0 * t * (1.0 - t) ** 2 * y1 + + 3.0 * t ** 2 * (1.0 - t) * y2 + + t ** 3 * y3 + ) + return xt, yt + # Determine the midpoint - pos = ( - (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, - (src_vertex.position[1] + dest_vertex.position[1]) / 2, - ) + if edge['curved']: + (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position + aux1 = (2 * x1 + x2) / 3.0 - edge['curved'] * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + edge['curved'] * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - edge['curved'] * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + edge['curved'] * 0.5 * (x2 - x1) + + + pos = bezier_cubic(x1, y1, *aux1, *aux2, x2, y2, .5) + + else: + pos = ( + (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, + (src_vertex.position[1] + dest_vertex.position[1]) / 2, + ) # Determine the alignment based on the angle pi4 = pi / 4 From 19ee0490962fd2db204f2203fb45205f7b347169 Mon Sep 17 00:00:00 2001 From: Sriram-Pattabiraman <59712515+Sriram-Pattabiraman@users.noreply.github.com> Date: Tue, 16 Nov 2021 13:24:33 -0600 Subject: [PATCH 0504/1681] refactored to Bezier curve control point function Previously, the code for calculating the bezier control points was repeated multiple times. Now, there is a function to calculate bezier curve control points accordingly using the start point, the end point, and the edge curvature in a function called get_bezier_control_points_for_curved_edge --- src/igraph/drawing/edge.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 88cc1909e..294e540fa 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -19,6 +19,16 @@ cairo = find_cairo() +def get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, curvature): + aux1 = (2 * x1 + x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + curvature * 0.5 * (x2 - x1) + + aux2 = (x1 + 2 * x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + curvature * 0.5 * (x2 - x1) + + return aux1, aux2 class AbstractEdgeDrawer: """Abstract edge drawer object from which all concrete edge drawer @@ -120,12 +130,9 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): if edge.curved: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position - aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( - 2 * y1 + y2 - ) / 3.0 + edge.curved * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( - y1 + 2 * y2 - ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + + aux1, aux2 = get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, edge['curved']) + ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], *dest_vertex.position) else: ctx.line_to(*dest_vertex.position) @@ -180,13 +187,8 @@ def bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t): # Determine the midpoint if edge['curved']: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position - aux1 = (2 * x1 + x2) / 3.0 - edge['curved'] * 0.5 * (y2 - y1), ( - 2 * y1 + y2 - ) / 3.0 + edge['curved'] * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - edge['curved'] * 0.5 * (y2 - y1), ( - y1 + 2 * y2 - ) / 3.0 + edge['curved'] * 0.5 * (x2 - x1) + aux1, aux2 = get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, edge['curved']) pos = bezier_cubic(x1, y1, *aux1, *aux2, x2, y2, .5) @@ -305,12 +307,7 @@ def intersect_bezier_circle( if edge.curved: # Calculate the curve - aux1 = (2 * x1 + x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( - 2 * y1 + y2 - ) / 3.0 + edge.curved * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - edge.curved * 0.5 * (y2 - y1), ( - y1 + 2 * y2 - ) / 3.0 + edge.curved * 0.5 * (x2 - x1) + aux1, aux2 = get_bezier_control_points_for_curved_edge(x1, x2, y1, y2, edge.curved) # Coordinates of the control points of the Bezier curve xc1, yc1 = aux1 From f12cd47979f6e7399783703df32519cf5c58b054 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Wed, 17 Nov 2021 11:56:57 +0100 Subject: [PATCH 0505/1681] fix: fixed indentation and added docstring for get_bezier_control_points_for_curved_edge() --- src/igraph/drawing/edge.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index 294e540fa..fc41b7b2d 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -20,15 +20,18 @@ cairo = find_cairo() def get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, curvature): - aux1 = (2 * x1 + x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( - 2 * y1 + y2 - ) / 3.0 + curvature * 0.5 * (x2 - x1) + """Helper function that calculates the Bezier control points for a + curved edge that goes from (x1, y1) to (x2, y2). + """ + aux1 = (2 * x1 + x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + curvature * 0.5 * (x2 - x1) - aux2 = (x1 + 2 * x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( - y1 + 2 * y2 - ) / 3.0 + curvature * 0.5 * (x2 - x1) + aux2 = (x1 + 2 * x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + curvature * 0.5 * (x2 - x1) - return aux1, aux2 + return aux1, aux2 class AbstractEdgeDrawer: """Abstract edge drawer object from which all concrete edge drawer From 9bbc594876ddee545bb021e83bc0135195e00266 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 17 Nov 2021 12:12:15 +0100 Subject: [PATCH 0506/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 24b6ef417..63a49e749 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 24b6ef417a2929a7b3bc2e95d9bd792213a80134 +Subproject commit 63a49e74932a73b00ae4b58d2af7721e4fc82636 From 9eb7a4299a613381ff7f0213cbc9acd15c5492c2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 17 Nov 2021 15:15:03 +0100 Subject: [PATCH 0507/1681] refactor: move Bezier curve related stuff to igraph.drawing.utils --- src/igraph/drawing/edge.py | 39 ++----------------------------------- src/igraph/drawing/utils.py | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/igraph/drawing/edge.py b/src/igraph/drawing/edge.py index fc41b7b2d..b72f1f4a3 100644 --- a/src/igraph/drawing/edge.py +++ b/src/igraph/drawing/edge.py @@ -14,25 +14,11 @@ from igraph.drawing.colors import clamp from igraph.drawing.metamagic import AttributeCollectorBase from igraph.drawing.text import TextAlignment -from igraph.drawing.utils import find_cairo +from igraph.drawing.utils import evaluate_cubic_bezier_curve, find_cairo, get_bezier_control_points_for_curved_edge from math import atan2, cos, pi, sin, sqrt cairo = find_cairo() -def get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, curvature): - """Helper function that calculates the Bezier control points for a - curved edge that goes from (x1, y1) to (x2, y2). - """ - aux1 = (2 * x1 + x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( - 2 * y1 + y2 - ) / 3.0 + curvature * 0.5 * (x2 - x1) - - aux2 = (x1 + 2 * x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( - y1 + 2 * y2 - ) / 3.0 + curvature * 0.5 * (x2 - x1) - - return aux1, aux2 - class AbstractEdgeDrawer: """Abstract edge drawer object from which all concrete edge drawer implementations are derived.""" @@ -169,32 +155,11 @@ def get_label_position(self, edge, src_vertex, dest_vertex): angle = None - def bezier_cubic(x0, y0, x1, y1, x2, y2, x3, y3, t): - """Computes the Bezier curve from point (x0,y0) to (x3,y3) - via control points (x1,y1) and (x2,y2) with parameter t. - """ - xt = ( - (1.0 - t) ** 3 * x0 - + 3.0 * t * (1.0 - t) ** 2 * x1 - + 3.0 * t ** 2 * (1.0 - t) * x2 - + t ** 3 * x3 - ) - yt = ( - (1.0 - t) ** 3 * y0 - + 3.0 * t * (1.0 - t) ** 2 * y1 - + 3.0 * t ** 2 * (1.0 - t) * y2 - + t ** 3 * y3 - ) - return xt, yt - # Determine the midpoint if edge['curved']: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position - aux1, aux2 = get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, edge['curved']) - - pos = bezier_cubic(x1, y1, *aux1, *aux2, x2, y2, .5) - + pos = evaluate_cubic_bezier_curve(x1, y1, *aux1, *aux2, x2, y2, .5) else: pos = ( (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index ecf546320..42ce51833 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -599,3 +599,39 @@ def FromPolar(cls, radius, angle): the origin. """ return cls(radius * cos(angle), radius * sin(angle)) + + +def evaluate_cubic_bezier_curve(x0, y0, x1, y1, x2, y2, x3, y3, t): + """Evaluates the Bezier curve from point (x0,y0) to (x3,y3) via control points + (x1,y1) and (x2,y2) with parameter t. + """ + xt = ( + (1.0 - t) ** 3 * x0 + + 3.0 * t * (1.0 - t) ** 2 * x1 + + 3.0 * t ** 2 * (1.0 - t) * x2 + + t ** 3 * x3 + ) + yt = ( + (1.0 - t) ** 3 * y0 + + 3.0 * t * (1.0 - t) ** 2 * y1 + + 3.0 * t ** 2 * (1.0 - t) * y2 + + t ** 3 * y3 + ) + return xt, yt + + +def get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, curvature): + """Helper function that calculates the Bezier control points for a + curved edge that goes from (x1, y1) to (x2, y2). + """ + aux1 = (2 * x1 + x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( + 2 * y1 + y2 + ) / 3.0 + curvature * 0.5 * (x2 - x1) + + aux2 = (x1 + 2 * x2) / 3.0 - curvature * 0.5 * (y2 - y1), ( + y1 + 2 * y2 + ) / 3.0 + curvature * 0.5 * (x2 - x1) + + return aux1, aux2 + + From 98c9123a6436483227c261b375366aee56d61df3 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 17 Nov 2021 15:57:45 +0100 Subject: [PATCH 0508/1681] chore: updated changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83715dd40..ced031e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # igraph Python interface changelog +## [Unreleased] + +### Fixed + +- Edge labels now take the curvature of the edge into account, thanks to + [@Sriram-Pattabiraman](https://github.com/Sriram-Pattabiraman). + ([#457](https://github.com/igraph/python-igraph/pull/457)) + ## [0.9.8] ### Changed From 50dc1a477662ba84ce720a5687df2067f99d9b6e Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 20 Nov 2021 00:49:27 +1100 Subject: [PATCH 0509/1681] Add quickstart.rst This is a tutorial file that just gives ~50 lines of code to create a new graph. --- doc/source/tutorial/assets/america.gml | 43 ++++++++++++++++ doc/source/tutorial/figures/america.png | Bin 0 -> 18733 bytes doc/source/tutorial/quickstart.rst | 63 ++++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 doc/source/tutorial/assets/america.gml create mode 100644 doc/source/tutorial/figures/america.png create mode 100644 doc/source/tutorial/quickstart.rst diff --git a/doc/source/tutorial/assets/america.gml b/doc/source/tutorial/assets/america.gml new file mode 100644 index 000000000..a23a3434b --- /dev/null +++ b/doc/source/tutorial/assets/america.gml @@ -0,0 +1,43 @@ +Creator "igraph version 0.9.3 Sun Nov 14 18:16:32 2021" +Version 1 +graph +[ + directed 0 + title "Cities of America" + node + [ + id 0 + name "New York" + population 8622357 + ] + node + [ + id 1 + name "San Francisco" + population 874961 + ] + node + [ + id 2 + name "Los Angeles" + population 4085014 + ] + edge + [ + source 1 + target 0 + distance 4160 + ] + edge + [ + source 2 + target 1 + distance 559 + ] + edge + [ + source 2 + target 0 + distance 3980 + ] +] diff --git a/doc/source/tutorial/figures/america.png b/doc/source/tutorial/figures/america.png new file mode 100644 index 0000000000000000000000000000000000000000..62db60fc800a9691c70f35bd3e699fe55d225e4c GIT binary patch literal 18733 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 literal 0 HcmV?d00001 diff --git a/doc/source/tutorial/quickstart.rst b/doc/source/tutorial/quickstart.rst new file mode 100644 index 000000000..cabc25eeb --- /dev/null +++ b/doc/source/tutorial/quickstart.rst @@ -0,0 +1,63 @@ +=========== +Quick Start +=========== +This is a short condensed segment aimed at those who want a very quick and dirty rundown on the basics of *igraph*. For a more in depth explanation about what each part of this code is doing, check out `tutorial link.<>`_ + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + + # Construct a graph with 3 vertices + n_vertices = 3 + edges = [(0, 1), (1, 2), (2, 0)] + g = ig.Graph(n_vertices, edges) + + # Set attributes for the graph, nodes, and edges + g["title"] = "Cities of America" + g.vs["name"] = ["New York City", "San Francisco", "Los Angeles"] + g.vs["population"] = [8.62e+6, 8.74e+5, 4.08e+6] + g.es["distance"] = [4160, 559, 3980] + + # Set individual attributes + g.vs[2]["name"] = "Chicago" + g.vs[2]["population"] = 2.71e+6 + g.es[1]["distance"] = 2960 + g.es[2]["distance"] = 1180 + + # Plot in matplotlib + fig, ax = plt.subplots() + ig.plot( + g, + target=ax, + layout=g.layout("circle"), # print nodes in a circular layout + vertex_size=10, + vertex_color=["lightblue", "orange", "orange"], + vertex_shape="s", # square + vertex_label=g.vs["name"], + vertex_label_size=8, + ) + # Remove the axes borders + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + ax.spines['bottom'].set_visible(False) + ax.spines['left'].set_visible(False) + ax.get_xaxis().set_ticks([]) + ax.get_yaxis().set_ticks([]) + + plt.show() + + # Save the graph as an image file + fig.savefig('america.png') + fig.savefig('america.jpg') + fig.savefig('america.pdf') + + # Export and import a graph as a GML file. + g.save("america.gml") + g = ig.load("america.gml") + +... and here is the graph generated by the code + +.. figure:: ./figures/america.png + :alt: The visual representation of our city network + :align: center \ No newline at end of file From 5a3a5f8ec62d1184173200e38569861632d78c05 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Nov 2021 15:32:42 +0100 Subject: [PATCH 0510/1681] ci: try to re-enable Python 3.10 tests now that SciPy also has wheels --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 34fc3d24d..6627fb0cb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,8 +4,6 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - # skip testing on Python 3.10 until SciPy publishes wheels - CIBW_TEST_SKIP: "cp310-*" jobs: build_wheel_linux: From 699f7150081a7e16c477a49607e9367fac08cc8e Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Nov 2021 19:26:32 +0100 Subject: [PATCH 0511/1681] ci: update cibuildwheel, skip musllinux wheels for the time being --- .github/workflows/build.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6627fb0cb..45d75f6d4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" + CIBW_SKIP: "*musllinux*" jobs: build_wheel_linux: @@ -25,7 +26,7 @@ jobs: python-version: '3.8' - name: Build wheels - uses: joerick/cibuildwheel@v2.1.1 + uses: joerick/cibuildwheel@v2.2.2 env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" From ca6f675c545d978cf7ad3c888f4213580162214d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Nov 2021 20:06:10 +0100 Subject: [PATCH 0512/1681] ci: move on to manylinux2014 wheels like Pandas (otherwise we cannot test on Python 3.10) --- .github/workflows/build.yml | 5 +++++ setup.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45d75f6d4..2dc74637a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,12 +5,17 @@ env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" CIBW_SKIP: "*musllinux*" + CIBW_MANYLINUX_X86_64_IMAGE: "manylinux2014" + CIBW_MANYLINUX_I686_IMAGE: "manylinux2014" + CIBW_MANYLINUX_PYPY_X86_64_IMAGE: "manylinux2014" + CIBW_MANYLINUX_PYPY_I686_IMAGE: "manylinux2014" jobs: build_wheel_linux: name: Build wheels on Linux (${{ matrix.wheel_arch }}) runs-on: ubuntu-20.04 strategy: + fail-fast: false matrix: wheel_arch: [x86_64, i686] diff --git a/setup.py b/setup.py index 5f15a6520..86b13d57b 100644 --- a/setup.py +++ b/setup.py @@ -833,7 +833,7 @@ def use_educated_guess(self) -> None: "networkx>=2.5", "pytest>=6.2.5", "numpy>=1.19.0; platform_python_implementation != 'PyPy'", - "pandas>=1.1.0,<1.3.1; platform_python_implementation != 'PyPy'", + "pandas>=1.1.0; platform_python_implementation != 'PyPy'", "scipy>=1.5.0; platform_python_implementation != 'PyPy'", ], "doc": [ From a735431a4f7a1b9c27037312e1e9c739ee35a7e4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 19 Nov 2021 21:28:04 +0100 Subject: [PATCH 0513/1681] ci: skip Python 3.10 tests on 32-bit Windows and Linux because there are no SciPy wheels there --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dc74637a..260901552 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,6 +60,9 @@ jobs: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" + # Skip tests for Python 3.10 because SciPy does not have a 32-bit + # wheel for Linux yet + CIBW_TEST_SKIP: "cp310-manylinux_i686" - uses: actions/upload-artifact@v2 with: @@ -163,6 +166,9 @@ jobs: env: CIBW_BEFORE_BUILD: "python setup.py build_c_core" CIBW_BUILD: "*-${{ matrix.wheel_arch }}" + # Skip tests for Python 3.10 because SciPy does not have a 32-bit + # wheel for Python 3.10 yet + CIBW_TEST_SKIP: "cp310-win32" CIBW_TEST_COMMAND: "cd /d {project} && python -m pytest tests" IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_BUILD_TYPE=RelWithDebInfo -DVCPKG_TARGET_TRIPLET=${{ matrix.vcpkg_arch }}-windows-static-md -DCMAKE_TOOLCHAIN_FILE=c:/vcpkg/scripts/buildsystems/vcpkg.cmake -A ${{ matrix.cmake_arch }} IGRAPH_EXTRA_LIBRARY_PATH: C:/vcpkg/installed/${{ matrix.vcpkg_arch }}-windows-static-md/lib/ From 666cbe2441026a4ef665339bed48c92c4d4a907b Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 20 Nov 2021 00:38:46 +0100 Subject: [PATCH 0514/1681] ci: fix Linux i686 build; CIBW_TEST_SKIP was inserted in the wrong place --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 260901552..de9259cf0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,9 @@ jobs: env: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_BUILD: "*-manylinux_${{ matrix.wheel_arch }}" + # Skip tests for Python 3.10 because SciPy does not have a 32-bit + # wheel for Linux yet + CIBW_TEST_SKIP: "cp310-manylinux_i686" - uses: actions/upload-artifact@v2 with: @@ -60,9 +63,6 @@ jobs: CIBW_BEFORE_BUILD: "yum install -y flex bison libxml2-devel zlib-devel && pip install cmake && python setup.py build_c_core" CIBW_ARCHS_LINUX: aarch64 CIBW_BUILD: "*-manylinux_aarch64" - # Skip tests for Python 3.10 because SciPy does not have a 32-bit - # wheel for Linux yet - CIBW_TEST_SKIP: "cp310-manylinux_i686" - uses: actions/upload-artifact@v2 with: From d3c075c36cc7b0d4417983767864d422aa9b1cda Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sun, 21 Nov 2021 16:03:52 +1100 Subject: [PATCH 0515/1681] Update tutorial for develop branch --- doc/source/tutorial/assets/america.gml | 16 ++++++++-------- doc/source/tutorial/figures/america.png | Bin 18733 -> 16021 bytes doc/source/tutorial/quickstart.rst | 22 +++++++++------------- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/doc/source/tutorial/assets/america.gml b/doc/source/tutorial/assets/america.gml index a23a3434b..acf490020 100644 --- a/doc/source/tutorial/assets/america.gml +++ b/doc/source/tutorial/assets/america.gml @@ -1,4 +1,4 @@ -Creator "igraph version 0.9.3 Sun Nov 14 18:16:32 2021" +Creator "igraph version 0.10.0-dev+63a49e74 Sun Nov 21 16:02:47 2021" Version 1 graph [ @@ -7,20 +7,20 @@ graph node [ id 0 - name "New York" - population 8622357 + name "New York City" + population 8620000 ] node [ id 1 name "San Francisco" - population 874961 + population 874000 ] node [ id 2 - name "Los Angeles" - population 4085014 + name "Chicago" + population 2710000 ] edge [ @@ -32,12 +32,12 @@ graph [ source 2 target 1 - distance 559 + distance 2960 ] edge [ source 2 target 0 - distance 3980 + distance 1180 ] ] diff --git a/doc/source/tutorial/figures/america.png b/doc/source/tutorial/figures/america.png index 62db60fc800a9691c70f35bd3e699fe55d225e4c..d86d613dbda293553b139a717c14b4d5d6a4dc3a 100644 GIT binary patch literal 16021 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 literal 18733 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 diff --git a/doc/source/tutorial/quickstart.rst b/doc/source/tutorial/quickstart.rst index cabc25eeb..1b48e1b7d 100644 --- a/doc/source/tutorial/quickstart.rst +++ b/doc/source/tutorial/quickstart.rst @@ -26,24 +26,20 @@ This is a short condensed segment aimed at those who want a very quick and dirty g.es[2]["distance"] = 1180 # Plot in matplotlib - fig, ax = plt.subplots() + fig, ax = plt.subplots(figsize=(5,5)) ig.plot( g, target=ax, layout=g.layout("circle"), # print nodes in a circular layout - vertex_size=10, - vertex_color=["lightblue", "orange", "orange"], - vertex_shape="s", # square + vertex_size=0.07, # set vertex_size of all vertices at once + vertex_color=["lightblue", "orange", "orange"], # set colours individually + vertex_frame_color=["white", "white", "white"], + vertex_frame_width=2.0, + vertex_shape=["circle", "rectangle", "triangle-down"], vertex_label=g.vs["name"], - vertex_label_size=8, + vertex_label_size=7.0, + edge_width=[1.2+dist/4000.0 for dist in g.es["distance"]], # longer distances = thicker edges ) - # Remove the axes borders - ax.spines['top'].set_visible(False) - ax.spines['right'].set_visible(False) - ax.spines['bottom'].set_visible(False) - ax.spines['left'].set_visible(False) - ax.get_xaxis().set_ticks([]) - ax.get_yaxis().set_ticks([]) plt.show() @@ -56,7 +52,7 @@ This is a short condensed segment aimed at those who want a very quick and dirty g.save("america.gml") g = ig.load("america.gml") -... and here is the graph generated by the code +... and here is the graph generated by the code: .. figure:: ./figures/america.png :alt: The visual representation of our city network From 6e91dea439a9b35da3954be24f334ff769e26f8c Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Mon, 22 Nov 2021 00:49:32 +1100 Subject: [PATCH 0516/1681] Add tutorial for get_shortest_paths() --- doc/source/tutorial/figures/shortest_path.png | Bin 0 -> 30009 bytes doc/source/tutorial/shortest_paths.rst | 60 ++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 doc/source/tutorial/figures/shortest_path.png create mode 100644 doc/source/tutorial/shortest_paths.rst diff --git a/doc/source/tutorial/figures/shortest_path.png b/doc/source/tutorial/figures/shortest_path.png new file mode 100644 index 0000000000000000000000000000000000000000..b48156a38a4b5aa190729a4cfef443bc940b9924 GIT binary patch literal 30009 zcmeEu^;cDE_b*@|V1NiniJ;OU-62X!NP~1EE!|*%fq)1bq)S3lxsim7pPG-KUdTz@z$U>)LqoeECHYJd4Gn!44Gn|(+Ew^Y z@*fv=_;T4^R7&|8{O57a=p+0c%T`ju9u4i5KJq_$nsDkX_$IG|n7V_KjfsP^zMV0e zmA-?mrHzB-D}(z^#&-6vY^?9IGe2f#XS{Fb;9$$g!t%dgz-(h@$`XTT`|Lt3PaQV?4%;)*rYF?ap zoKpBq2jdLICAdV)Uj;ihf_64<{*~UX@#N9!te(Ev>8W8mzEHK%xls0ngCyW8<$GMw zJLZk%MEBtj9|vgOFvHioXjl&LgIYAncpUh8RRlx(8s+=PXtx(d;Vb%A5hD0{-RE){ zuIM#=w1-{3@b%LD|NpE1f0_QDTjG1VwhyjrLbq9o(;CUi!Nx*&l7x|oiGi`PSPY+| z%R-FvV|I3>@@M*!)6>&~<<2Yz8xu5wg5=MiKaY-ydDWjuOh`y*9!~T5F_wb@ZM1;v zxA^$`UKeM#3=C~B^h~S+b91p?{iH)vf2q-(=!zqBvZ^F>G>b1IBh$;Marf)@?|0|h zqa+-537K`PF8h4?NoQg*|06jizT50EH+QO3Ps;9kpZ}Xnn2HrHuTdFUXhHUZcpn=qkAMBhH;<$!Mluwj?IMx&*@^b#h#%&|1ipar1b(6O5znv#MKLliv%MLsLaGII#^-3i!GH${68WYnV z@OwK}+b)6Y5rf6Vf zyx8e%(5|dqZg{ggw2LDf$zgnU>=??TTZKkXC>C?MtcyP)BH{}f?>Ao@qUqrS`>MO# z1nfHou3j+@MvC)n3vQ`Q*xsi%ztoqm()z*N7oH}^<;#~JJFTimR!pE%RNrTOK6`d) zssUdD>+UfD;k?N+tHGKPo9dWbeb`$>SPltld3kw@edXMwUPr#@_$FObGv_&bZrm)o z^%E=hY)T=E>N+mJ9C<36!`;m5%G;64^&gcwfZ8 z^IZ9*NNE=$LgZ%Lo2Jkf#U0e`AKqP(l_96YKGl7R+VgMBbfY*Kp98L@re;h<_G5wH zzkeSd9&Yyi&~P}N#fhmfc`41tj_Flfxxz{&)5gEidy|M!w<_A}?0Bw8_vsx)Vq)UnY;DYt+gvgi zuh*V4RkhJQqWh#9=4-pu`3NA4zXoxmA1vdvfn0~kFUfCx)vdC+4hLcyw_{$fTu4* zzDfS-WA6JtQ0mpz&7uMl*YCZmOS zJ<1D`N-a$Nt}vUeDx}u$DwUZFAH|@j~@@m9qbZKLc*@clrN%m9y|~X zNR^M((@bFumya`&$qDS#PI$+4SSf7Qp?2vi?wvb7{I`8y{2?fN5fc|jF;5ugaQ1fv zb?(vb7hvzpE7Fi2sQfxzZWZ;sG*HdSA;qdwsO>v%NN;biTDHdBz^<+r_XJ#OB%VrC zZd-hhAW|Y}KP9!$eUF7zp#BM;GvkC)3{sECVSWl+mX%yOSl8(uk)=ODQe zL{V>OWpOZCZNIYdyAJc#$H2h;p`&M0dow}8-36W!Eh=6~A{_%;Gv{VLA3uJ?;Eki@ z12j!{S{+1QZZ@@?(landP#E^X*`_B?DSY8vdyYHiwQV+~P1@V%;mog>d!-^*4i`2i z)Q3WUc(rn6>M#j=ixwmAK}AJHd*OE}&~bTbmDA9Wd24NVtJkdS<3ZUTJROde$wn4Z zu0K`>RLoRV7&y-vNUBy8;}@*De+PJ;?AxvMXJH8DJhlYHSBc`5=qa(nj9J6+cDT5g zFL>MP-I4kBUYnu$WVeD9#hD@sJtL!fs3Dx) zirQaN@i=_~p2wk4+%_)?=lzS1W>pjoF!HZ@iYkg?;D?{zD!023z)i|aVy8|?i57HM zrCA{K?HS59ecjdFsjGwBp=C1t_707M^n3zNU$k(}*aMxT_T`+jmrJjH9=dwU!R|@% zIAB+aK0o7>ZEs6cUX$&J;XfEg-skM(!1ZtvUBzK@=YXERa;Z(@=m?+hq1vJ-XJ@t? zJ#W=`O=xB&<9xv8?DVIkFV|m+1{6=Y{uEZrjSQ(sAk>F#q2kt~f!cvhGTkXWhpoLB zs0Ei%-`HqiXz{@p)n`%JvCZ4VQ(=X(QBtt56wI$BYCl(LRkchqef`+yzUES2 zpkoP%PiW_nk8Og=zx(2);s#pLW&>Rgs()7y{fi zsqytXrO&b=k$oV6p`DhZ%Cr`xVJY*wI>DTS#K4f5j`L$;s(D}f9R}4egWp@zqlMR( zjm=vzr4yba_e?;qTr=G0H`scGVlilA;sG7ylTvAXv7k?%u;e=WK2>_;)^x_8Pz_n% z8MfajE`~y32LuP>F+~(BueuW-5iqDScI{xF{moEgS#KPl zZHZ92;x>NPt)T2Wb0d=D6%-@x$OZRaGU#EVfJxFE8f^#p8Pf3#n4(@M^fwJ#NjyoB7vBY|1Qu%liugRTiW{(;Gb*YB~RHM9Ym|h}-AbX?RQa ziamtYq!n-D{oYA?k$}2u)fs-4Z$R!oYLJhnf=6H#raWgb|~pFSz)WRpj@ZcU3-R8&BN zWZv{u5GCK*+WJDyAC~+)9IpBk3Pn(8-e+KSi1F<<{`THzxwm#xA!$0Xnc&kZm72D)amA~-tsL&{LphGcOq*^AYq-?iyPdv8hXN(o{jI>!G zS{^C3p}2SN9+|KgS=V~OVo&O=C@w21cBX)jlSfBR>$9n!PAl}7$I6^jHS$f4obD+& zM92V7O&=D!rjQ_dWqqQ$^VV-1v%wT{;BZU3d+h9Nn>)v3A*+n4*;-O_EvVOyj&oZx zOha${p5h5Kib_hahSCYRXVxbV zc5(*8`ef5AI7mckJlnH%`+wOlbS&(juMZdcAMGqKs4X-$HhP?HHF+E_rLytVq1D&d zKX%`@%+|fgXs~S!W7^u=YiOL$2~L5=;IK9vu*UEFc*mR6rLNSk!(1J|$OrYr9$>0w zY)soUhvAtV)KkuAnWI0oY?Ogk+qc*Ht#lXSeFJWlZC|ceMF~bMqFOboJb2HJ_uQur z#>$;NL-i{1XFm#C=dT^`%XYA5KD?i%^ZBu&}V} zq+DN$tVXI7Z-|lu-)Q!NuTH{` z^EEUyR)%von+kWWKAHZS|)<6q^!*D;@mS^ zrxM#Nt`6B@FAL4}Z7%F67@z%?mX|kO`kTzCQ}Ml5*(&6_VzM-=>$Xvu(^_Pz2q!0J zvire$D1%zy$GEWk{Sm!;1V`5^at7fHVRHx&zAjrM3I#L`&r<8*BB1cD%S}ytz_k>4Xt(d>0+wmO+3yXAa z7m1Tbb93qKo@d$Tu8||$+31_UF|*sEdEYe#kXAb}d7o}!;t(-EgCP*La_!7Oljlsc*(6Q zHNB%>dS{KSbp)FBo)e*&4Hpozn|9Q6bOf>*Hlj_=&OVcr{5WbybR>nC*N7vC@33>Hs^#cZ9E`hHoUE6J(kY3ja*kDb zk_rn81FK0T%h0VBGHm?PeN_5v+>C>S%WC+UnORyMd3#&i7Z9=Z_81P27#RHVY21` zkReq*u5n<1$OlwWo>B8RZx5yQkzy9;&VwT(`F3+H9$5_R>>+Vp$Dd%S=yB#Vv$BX; z4K8PC7IzHT`x8Bvl++uqa0LmZ#ydYbS#!8G^V*?zO=w%K(0)PAq$3&&k%sNC=T)*L z@3Jky=4@H+%RqUr!*duPP2_;h;(c)r0+Uv44oHPeoKOsCenmw^V)ozX42mUOI0dR^@Gdda#cmGpMVO(H@n$xs{Y;|=N z&E@ov6*lHW9-i;JyACJ@#_03Y9eDvkLAcWoc9~B<+`LiOQdudr2eJ3eSl-~`~+xPCpTJ-N7@2$Z0oEk2)SSC>Y z19Wx^#Mbo6N-F>m`mek^gX#KDU~lAgjaD9&-xGABT@mH79E|5+xGk!N=%&Aa|AuA| z7#hxKF}%e|LRuH#jNf(Q#N8X!ZhQ3OzbWHVQ4r zM?8RJYIgRAomiQvwRK=|F*h`>AJ7lb1UwGELXZ39u=JP8+t+ET_6lJ4V4ktRU0Xwg z@2_8vmwSIE85keYEMN}wWvD%$z(`;R+CTt1HO~87V>zPLAV99pAN5GZ%T0WqlRk!Yo|NFL0 zf#garlll1h7hFwT^^HtTn^uSNw{~}xRrz(x9I#keSh#G)wTuYc8k(BeP1^2;F=?jk z-b>TSr`FfkFR+==u{l0iNR3uo;uRjm-a@oEz1@Dn`C?Mg=lUofGa|RW)u9kEe?o<1 zX#zChURc+!8v#dtNKKutrGGvQmgT#vH(-O|`#7QNPgHqE2zytt)&HOcM4-EQ^QMne z^;rbFsWS9B46raKkru^>ouQsf1?G`rY2w=jfC^jNUw7 zy1M#f@J*=YW4{y@a-xBoY6>`Zx>~#EE2UBxl#2JqARr=Yf+mG-g&%fH=ul+tbMHWz0GB`h#mdT7oXJHQj(^@@?6LDZ!p*eH(+7-;O z5b;2jLaq3qLQ~aq4d~T=P(T&cMf2LH>8|&uH6s1%-F_ZRs@1@?K_>ZJR(e$8mj#hi?&V7ESPyL1&yA8M#sEexw z#ZhCUqwoZ(zkmPURcxaR>lzy9+T7fHcycncv5~`}-R=D0Cb>aZ{1Y-E4|U%8_4STg zUmVw&0B+?>nG5xoFaIV3#wGe!&mBG-y^L=k+fI-aycMC4fg&r-+S=MQAm{Pgj}Pi@2|qUPrG@7WeP`I$MnOU0`aJvI?w3-Unox|1q z&QR{No>cjnsj1(Pg9xlgQ0OkMuCGB>i{Ltn$* zg~yz4)}5#}j^Q}pMhv}eu+%=)E)y2>{rmTmtE-tI!wowxi>+k=nb3LMc4@;|bXB48 zsQ=;5{6mCvp={jNP`am3*DBe6HU-^d#}dVG<^}=zHv#VfI9Sg3Kw2R_dGG@XdiSyC zp|rn&k3m;z0B*BT=>Jd2gXJ=3Q7TQu@*G2Z)<3$l!+m!#p`hr_Q3l`{vJn%WexT1% z&CxFR1DpeJx#$#AEw-5e8XOL2DW#zWtXvSNqwB$#^G>B#!rbz*^iRcPw826PwS?F* z8tg#ehNa+$=39+$iMSgN{8XYrG0?w+tiaaZ{=vhCmn9@5M9Rk)f&55?P}6z#Us%IU zl^!>cl9KjRdwXMGV1!m~Bq< zSra`e-?7v*aB$ca_QJ)VM$9@TpxDrX@Vc1$TZpL>CnKVw*j+aDOxh!pM{7L~Hz{Wt z10De7MCx~L4wcboR_?TBw--dT{3|zCVcj}8IXTzs%nj(nu^cEJWo6~izka=aeJXzT zi=RNN|1xU6O~P%RN#_6MOFgV-NePb#|CCkRcUCZa2pJd{3Z)5ncUOm$N}uMz_Q$Z< zV!%csBle}GrIte(LqkJ=Hc$$)Miv&WZ4vB8&`x4WqZPr85q+^^iQt;oduAv^AgdibSu1Wg~tL7Rwqh4 zskyIRI=f@*!`inHbZ&P8W~|Ie*{>J7D3PZoiJ@Il1jL*+za%C)+HCMw0s!2HTNH=~0?+`81FdFg zC;|ispTpwjXd(~TR;OoYN87C|D3!j2&bTH(I}m>@1PLuhX8tb3w-=`i=RE8|Rv*~w zQ)2h$&$B|HrV_ZfJ{ENahwueF3hz^U5GuP%Js*&7^iNm7b~T2avIsFmHM+ zc^z|__ul8=2u0`$@BTs2uf$1WsIqcro4J+fo!jrhS>K907ot)bexE)AJp_`$*=1_v z^Ps|C!DEQk?fm=qn?jN}D=a~Bb6X6*^Iae_!e_h4#^B4)uBZUxi2i}y!P-a@I0dk= z7V+UtTdXIl+5zCu5IZkBI~!X6t{F$vDe`gI1@4|I%*@R1eSH;D<;X?K zyR4!4d7SMHw3w+V*tWH|FAe3>#GiS`*=MBT!cwBLH1frXbBCbQ0PLsOiHWrWtNJeB z$_5gG3X<}FbUNsYGKZza{KD(7Ml_Efe}&jk-!jv)mB)VmI`C72g^n0L-VZLW2wPc? z1VfMlct5gJiIC3_@H+jzxM(I)PLK`}6I^!zVCWavJ+CKg(C5^Pt;axLq(B|<3CDqJ z9_P7F{N~LY@N;hZKsX1QUP@0de)lemh^ne8^eFV<+>*{=CL916on{~@QqP}XDp0%5 z#l;10&0%C@1lmCn5W5aME_7OryoVqy>PJRM?d^9w1ie8wW8vV;R33~gAz(}l44zr5 zya1koVqmi*2@o&E_Uf-`>g`2jvPoxbR;C-XB+wp^60r~+n)jxm zo1GkaL)AlrQ;l6AM00j_Hjb$W)*R}1Cvg$zj4K%2QlRLz9XmX8MGtP{<43R>UWFC? zk&=P|^+7<$^zuEJ40)2WOrXuFXlXwRc^=bF*@4A@plGp8TzVmK8I*NTx(b8H2=V5@ zgf}qu1THLxU>fka)ruH6t%f;t3H7kDfF%RR{|%fDWe7W&nuq5u4Gj$eLu)%wOqS}? zZvYquHa53JK)oY&CO~`(CSHQOH^ng38Tw~<%dNj0B(m8H#cb~%d4r1B{x3_MsJ`U` zA_KOg$>HWyi@k+sPyCZhND&R?CEGF5(_>RmP(b9MoY`jEm!|NPINg4wpQ)5Vu^xm) zxD)grD3+@tYr};grnAeg5XZx%&wy-4_-%tR&}$k2foPzE8CqlB`MJ4>)A~;Y@!S@i zb?{1IW5!%}ZN~Jr!DP4y@CRA3_1Eay^Yden|C_tJh=%Yfaz5}uzl7x8#x0==rDfr! zuEG%;w7p+M#Bj(1;) zl+$K_V5V0y;sH$9JUD2Yn24L5H9`ZTD*^}s87fo3*%mn&nRbWwy^T3JQ#+Ec&7yrnuE)>vOZ zxwh7xQ+-bFU#b(5k&*HDtt-OaF~t-dB;)5TXcv^4Iu%&TKEOfr^WF~et#mC#^1lueIN}bWK^$#1(1}Kd{J6k+ge-u_8+#y zTFgr-@+t8NhW2R&s3Cwwbf2iGD7GSnSy^dmY^W^Y8&8RUC_oisT)K1wt}o77ZL!+i z;-VyIZ#K^BdK=@FE-o&%P+*~N%82>Bf6&)ZEbCE zS>-JR1ajfb+6J4Gzb(frGAH(HX1T@AvY!P_cQfIV;zIuUS3t-SpFPai+(!3!a8#uy&j zdLR*KRQnz)lvY7>4twt@wEt_jZY``8Ph2e^$5I2&1RUm4$Ca>_J^~>K43wP`5bw;S zkBAa3-S=-#k4CXIdA#TP5YS=d1CSwylGiipc}Ab?k~J#b9Dz?QbtSyxm^&i<7vqU} zY~!~Np&{UauE;fRCDeEvjtKzM)X;FHYri)rFc4wuFW~ZV&dyIZz!<{yQO$a(P-BZ&^K*~AKD8EArmYR= zx(TDrE++6<;2PM0+JL7hKt@jfYP5tE4cMCTK(;pE1vdC4E*({#C!wZQ&|gx)NS6R#9=x3qM-$w1_uWRAywe+pkfG+x0(GDd^&M)7WW0q6ANiO zK}^@<0D{ECf6NE`8)<>B&JeN)Ku*l(Knu+`88pT+q57Xfs9PXqNNxeBzX`?$&|x)_ z)YA{e<8J;))IEGvNFtC7NPqCe!!9vq@Vcg92F+)`XpDcOBFRqdcm*6qnMe*Kk1)=u zN=kY_Nl6KC+g!^{PN!MKh>ni#aelPWvb+XeV;Xusuw0dFId4dR=iI`WpKAVdsgD*| z&Lk*2A13VH3MAuw3l9kM);;Aw$BpqASYA5vuJ`L87B-hs3vwg8t2^LsJS$RKY?UBy7SP`*h! zQYDag#Z{&)oPh%eA=$TIZs_>(k=s_)dWf zi-rViuJ>LU8oq;0)IzXy3vw7J%FX@#c>qtJB7*Gja3Um-nfLI2y9ZksdRhwOX$u&v zASjSD`tZ;NmC-{1;jHwCNq~5W#q;#XjJFptx;j$4xLWcdt8YQcD};ajS_5nrtUsZj zKYyZCY&DWH0%bw0X8(fseDtcBf8a?T9nJ)y_U_ThMeBh$|Hm>|-u)02MG9#on&ns- z3LXTsnm@O;tslB<&tg@d?UTl@agGD^Loxil1&ye_uI_;v_dJ=4gt%6);;4?7H%K^; z0xPf_Qk+|^_XgKjAAImHcUeV`CLMR?WsO;9@|&c^f-5Tp0U9n9Bn5MWRcR+CI`FW| zzXV9??c2A}Y+t`t*gquX1lJLG-1O4YPfKq$R@SLjmg@S62@2?5D26*Mt>Bk{Up(i= z@hv#`iMsk-d}^5&MWlpcNYK$w0yz!1PucL+;qcbKU=j#4K$1sHeulEbx6sh{ASeK% zR7@n)z?1@=(v>PtF;t}{S&2Md^SypuImc!VWf-BRGW5h|`snVR%rJ*vTH282WM!+%-fW{%HUzxcK;&L?BOaUCAm| z&Q6;g1sMwf9tn+}L5&7%EJ{Yk>qJCEs0{q0WaXcV5Xg}Ou`@1omXWy**~l|7F@m34 ziEJISE;GqBB1IJwH1k<}kxWB7)q!x0FdLH$q?; zLwCZ9p_gB!Fd<^rs%dTYhg~%{=)O@Y0eb}KgGw%2LJC0m)XYrX7h-lqpu}q*3BtH* z7_)YITM4%^2)IxtO+p_aJ$4W?f{f02j#2sXQ$T>q%ZTBjp)F`78CnYG%^e-{3vu2i zgTM4)XaWZffs>$n?GTC5DI~scA;9+R`j6}!{6z@{Y?<4hIhb1}T_MdfvaxIpPQc$?|WDMBlQL4rdO4sYeifwV+Xa zK+jDjnS*o?4l41ciS@nW}uv{MeGAn1+0Grrv)K2A9}SkcgSs8 z_})TAQ9XD7-c7|#s0Jh0i!JuQZwa5`g6jhfEen3lxNtk)sc3(HA4GOIh!Mro?c386 zJpBClw{MHW#i9)Po!9HZU;z6iLtW#1ak!8HMjtNeK}OrM5lHI+o8u!$(8R<@7 zW19)zu^IgJ1lA5z&n(1~*So9rM81O9TItTTM+)#w)7SzK8%&Z>EWR44|Hz zqz9l54F~)*iP965?{lZ|6nJ?0FM&;ob&7G=tT27%Qdv<#KEL5%oAWt!oX#dW0O zNlV~vkmiF1j*A3zc9rbKKJ99+7*HOxLAPRa3k#D0>FHG-eI|kJftUk>xrV5p9$>>F z>hg5M`+}iYt%$84*)~LkThEV;N+qd#b0w#GdTlLB<|8|tA}AUYbkC-H_5`ks9J0OM zbQPSg@f#;li^PI%4hdFWe?gw6tL5CRfx(5(&vf0NQf<7yx(bWXVpRhq0tTh=Q4B5R z@84erlF<0}3XUWg7d5|szkxvmFbxvy923(rU=$7{%1ns7kb;nUET1E`yu3V2T4dQd zy3#z=#O8+e+drUW^tI|c0J?^>AJ~*Cfu*ITcw}T>l9G}>qp;vykCM{y!G=6MWss%l z_gPtk!O&obXYVKZ!=bgNC}NXb5!1aL|;$~2vkV6SrPe0A!?sm)&NNG9TpujAMhQK z#1DbZUAZo23Xe_nqM{-+gxN%>((XoGE<=a$gL|OrF$-)S^SUdpFaQDjm;OzIs_@yD z)`!R%Tt8A}Q-1wHMDKwv0#*wh;L>n8VqAb{qf@%_qZ0@?%@?;dpf$ypkvf5|84Que@|pG(%I(B|bDst z`4&gGHERvgSWLttz^MMIy#LTVwX9E3}NKx7UShcY1+ zB=SKZLufBiy?@LBR;&TI(BL5}zf$m68|JL2tUQ_v*WJxyi25%93u=rVJHtPg!Wi>; zCLsaDA76xjKldMnzFPryHu6&wREVg9Mot)#*X@;zCq)A-kP-Z)N-M_H*fT$3+ywhH`Zt{0+Lb? z=HTR%0=_GNSS2Sn!Z=`BVf9mvM*h5g{tsVVFuMO|ya5H{4WA%u+=0=erG~e>#KVaf zP{}nD5K*;OwIf`-)I_pR&S@tN96CqqJlHmzy)bEcgh73Js#Y76@v; zg9i!to!&nHvl%e`h<3QsDa>m>|0R9z;{q@{J0cdHS>OX`%D{&~=KoZEZf;Kh-G@JjD1Gt3m{{ZVfJK(;D+IN$Q=R`hyec((58hj z%Cs1V-*NfJalQxT^(PT*#)#>t>vfcDhq(^w51D2-*{CLR5rn6o2yQf*MjNcH#Vl=7dL1e#6YB6TpIC@#xk)fsY^z5Kkbmw7fiD?>EM{%S-@9me{qYrUnM0 z|B^CA!Z4&_V9E+o87XOL`d=>@nV2Af#fc}!Atff(&|4t9ki)3_3U?S|rvj|nbdFck z3mHs+H1ZQZ4ZRip|1p@L!1TVW65NQ}BqWh5Tf?w#-lV3c{*^1e@37J@d!#*4@G4}P zVbeDZ79un{S_YaAT1rYvY3!@HR7jP8Gbdg5c~c!}P_fg&e?{>fu2@WR@*|kUFc~d* zjSMjizma$#{zVVKO?u+PDWrP-85s;{KXxO8%>`uzg?$hcfn``GvZ=pa0A+#2^3HnI0<--&g>B)-XTs4GGKUbs=KG&H1T)O%Agg;oZfhJ;zyBP2gXb8TU9c7OJm!Fz zLm)E@x}DmPR>4Z%r~%Cd!27%^R{oS5Ygg0+s2D^lm%1uk?UU`5ym*{e?;^p=#rauF zE~uS6h-i#=lW;|~k>qP-AgHEB#Q5?S@Y?@+|37}rj+EMW1gOJth&Q9_L>Dp`mAIfF zn7`uv1Pcd7r9*QKo3>%@4^29Zu^GAygy3|WYp+E=UZ@sZYa^nsCndnn68a@FwE;LZ z-^v>Urzk;Opuv!WwC?o;VB(^)lfU0-ReO5l5kl{!XyO9 z!Jea|_qb(BiO;a8Wxm%yH{jOZR6FYiILySb@w9@rQs@%PH+je|C(`t z5Bg7Bqg{5{Os(q{3CA^|x%~kSRX{9;jQK$)6)D&C03+ZFDYu-fQ7Xg# z3@Chb=h76tBLLSq4b1HHXdw=bhli&NdQwcKlYs#hm`PL+aDjM0qgTsr0~)c^ml2*f zD1J@B1=L7He0;nX{@oyQem|%ksQsQHSKt+P+IoMw2>1T*J5mPkg#Q5f$A>e7WZFwh z%|KL9hUENCkOp5iH8qWietIq8CRBaZ@iGI{6sp`33k$2IgFxGg(_*X)IZlFBKtU2a zHiv%&R3C1M*BKw?O)|6s&lqhjEu9liI0XR?4aMQ{N)Ade116L7fLVg;UE#MhCD|-+x z2IS?-9xi4TQ@VztYiVB6n>QQn%QCiy=d9WWOD`Rd+8BG5EsQksX4&j1eRz|KHsPYe7t zV?B@CGr+l-T385QAykKPg}ZD<*C3hXJ1WPb#3q7+nJ}=35CVWJB>9BCizK~n`>V|m z6>SR$b~iTq!KT26Nm)plt53IaV9hWKo+g8H(g{BpDs<;|ULSn~4jL%>agQFn@PqLr zxE>C(E)nL(I>Eu>WoWbIK(*N+g*!i}z6g$qk@e+HSDaaE91LX+0R7x7A9ve+{xYj+ zC!7S91v4MyGc^>c2TU}eAr2qZB%}Po#vGx|e`f*OTUw?OHvt0UmV7%nA?ZLtmtei^ry^fzy+M1b5u zGMbbWyY>i3j*nzINgeYU{!I?TwL8STYT8H(+u72wf#zM-^#$^oZ#(&sxw;E)48IzuQ*6fK6 z*{5u6j*|-=9U9I_pBb-?S4P1W4*CB5ni)~m-rC5(q|lahJWhhUTDF!xh+=@Yr@Z}u z2B7_+ox1)U;Bj6j1D6QwzwnzB-ht%{wwA47?n@*F=K>8u6K%7*Zz`cgK7h~mlA6Ej zatP#&aw_H5o`TbUZ@BV8=+zKO$|>%X5U1?#kyWRv>_DaItC!l`$J|~Y^@t99 z!eKpnsC%IW2^Z4qfd;CG$Ar--`GJ(2Szb1`C7%Gw14F=#fB&)#+*7Bl&&bcuuj}hW z0h2aPYi*C>Mj8RoVmJ$au6yAD{B^z3?ezn$u;87#CAJ!ToQ&J9>A#CCdB@lOhQJ9Q z!sE_qNd6q)3~FrF1tyjQIS`&ZRwIQ;dG@l%IZR`E6|cDgHWt-r)tK_&(C;E&CuBfb zivtqs99AMM50;8d&`p`Qzs%7BhKx`Prh2}<@J|RF)j=E$-`JXuva#D;RDiq+u6!PX zZ6r>Bh-Af==o<9ll9GZ)>+6Ck9@UX$vYJ(PHz0bi=&Rr)AWP3>TqL#Ds+NOh2Tx7+ zZpWFL;3mMOkb`}aB`W<3&Iuk4dFFj?R?a#9*FBQYrA$D4f^GU>pf~?tdU&*6z7e`+ z1yTiM04WHxqbVZMiaa3Uib>)(=G!9bfG@$>iOiRUpMh8-C*bK-jx7YgiYHBhAuzYR zx6<~q^bNgSpQB=3xwRm?l#xvEz1!aM)XGXm$N=XvKgV0DVqjaJpH)x37NtreV(F3k zE^G*AW^01&CH-dx|_nm}XHhrKNa$$JtQ5!%w+;{@yA-Ct@-|H~3u?Yfi`X6T5 zdV(-Z2!IuAjb>6=qHneL9a&|QMor_Eq95eJx_t77 zJA2bLVF3>72LuFUWJ9^@rp$4?%>{FKz2>_EX^ToH&g^UcL4YYtFt|L>Jkj)3GJd5- zUihs3qS!fZN&)3c(NrBG^`KXT_OH9dSDh=*l0Q4>i#=2LhI{`$Y2@xWM#AOWcdm#% zx$^o-G8VRg!53yRzWT09-$br`jcpTP`I^bRh{4`0MP1$LxaMEE&~|pQux;z5)G>O% zkW*2c>8Pu?_S4TPf*oq2TW}=gTXfsstP{3I{Poesg^0#GZQA0b?w!a8mA5U?4T9U9WS)#VNtn;04~D)+deoOKmR&l(X0g{kO&9C(1-3(CJK={6W{VFn!ab-zBugkD~r*5B#C!$^8;DbDs z_SopFfqaV8n`cqq8Nx3dPKCw4%x#iRxRwc+BxI6te7p$xi-I#c3e>_j?6Tzy9%UD91}+^ykvK#=NCG{ zEh#V43|ZjYYOIeqAYafZxXVe(H3x-m+!f#CuEI!VkO4VoBC~E|s^LIW>;tZ2l31|~ zU9t6X_0__Ob@$9yC(NfS@0=y&Z+j$T2u_R{ZwO)~hDl znzUxH?2lU2da;#}h#lC3=}Vrlnk7EsyB zzPb-OzQ)E{D|t#JmXBK5f}f2&cy}I+GDIT8W#K05js)AS+oNATnQY8=mXZ6D zm@C^n!g0Or$+!F_=+j4|6l7X+(Bnqt%VME+zKR+Ne-nJ?2Y$DW88*j=;%<8$Q9U)e zd_K3cO|<=xlM{jJ?K$^?H4SnUEI7zyo)P`AT|)BmJc}8=qMQq#5pKig5N-+ri@qt8 zca?0C>F~wePPDd`q*o0&9I}XDnmZsJT77+kOvssag)rQpGo18OPkdP39#cHMaks6p2_a_%xe>GG}%Ea&NKD(gtw}&-qFYd*&yz$DjuC>=U z931Tz`np409Cv?w@Ct0OE6!$e9^@zS{9t{_+4%tTT%0ZA$3vQ_b*GCBZn0-v%QThJ3spb zYm1Q;X)pZnmra?);F!D-hF!;)zFso*di8ta~}w= zjYiHD);W2|F0mQIAquCk0YW@&?(&OEuS-5|RjwVHqac*+f`_Sjd-0zh86{*Kz6i>oYh8^;08*(BhJ*e&I{@Le3UOC_8VvNo8iYfjwvZLcWIzkKZ;~G|lyCC-;pzp?=~=8y zR^d{N-d{x}7=^V<>2LmdXVD26S&nWu8IR!->d{HI)-Co^`f%P}TO$Goeq6xUX1D&# zZ#R2QNJt1yr*bQq`+qz|5$U+Rf%I&9yVg!2K2#)hz?I2rzayovp|9hDz|%841ffy!rR9zIjjq)c+<-Cj43%4Se>-S^^1H zfs|%Ou=`T19ggfWEOfFt{}CbKGLzJsj61;0hL?k26FA%LAvT2h6XQQ$DWiD}+py)R zzMa5G)u%!VFUdphkwe>ahjLHFh`SV>6$v>ZVWBmCokBe(jNWwu5INC_b5BVEm2b+O zY0@aPWxjW`tpuG;hPubF)A`}5drEb8&}}jQJo|;4*mr1cCs?hmt&xvJ4k$qC{Vgo4 z5hgDNt33JP2XMJ{DfjX+U^oPT-UoaaxCyp0@?fnT$g#lq%yZelo~^?pan|3d=j3(D z6q}=6RsZG5*+)Lo-~Bn&DbRnNR{OUW$;)CQ+D-a%G@CSSDjQDnY4Q!Gg{7n=U1nVw z32{b*blP{hkGo4?$h6k$&!O4b6B%lJay|#dOoYLNE69W(sNZIoYAf*)+_CP*$1633 z@DS$UVmD)S*^f}M&m^?nGF=0GFKSAJVtzer*r_`|xkjIcg2nRaa4O-oNyed zjyO+U>-vq-6w9N@u@A%CgXV?XJ?m?icU@wgQv#aYH}#7CS9@3fj^)~}UqmZXO9&-% zN$C|ziBMh*XjCFoyre|N$efB45z0`8O2`n=WC)cxb7f(fXUY&l=6&A2wU7OM$Nm%c zUdOS1SjT$2&;8ubeNE?ep4W4EejTP%-AaOx_9FNOphYa|I&=Vs1_KN{qR9r&5Z}@Q zmw~u25OxLM(LLx_OSIqby7Wa=YPIWdw{-9CywA@V_9mW=-J|S%>qpeIs*JAe?5!JF z&1k1Y$XTr;{WS0QcTT3A*0b1o`RHAiyAtiX%f!VwZ;y^fPWK3veDZ7u^VhbcLgMVZ ziv<;YvJl%8(d(6L+9|+4dO*P9qf6!e7O~Eoj#qVCT?Y;Z=(ylsnZU~&cqb~d*n=(z z(dLkkS&dIzN8D3iC`<0)qt-AK*i2S;|m^%-)NLzhB zH3Paqqov$Tb*VI0ziv0kPk>HIt!~?0Dz#cM-eExIwN;Z30~1S4s)0g|+sFnUo`dJk z2`SJ0R4vv4g2AWkp$H`HrL%{qM2(uA50agiZqs&{m$C2a@4#ozo;5pZe%$YU0TRW% z7{>u*3LQ`0nu}HOD!V=a7g(`+wK$a;fOh&=Z>0vFN5Ux2Ex!5v`8Q_?GJqZ;-{L)e zeSKcVtJ|>$CIPv|N4==QX}Nl8sME0v>11z zlUS?+mI2>R3odU~Zkd;_N@7)bIHp&(aE1AyUXC^^53Pj>_@^TUByBSQmF$C>G@50c zO0^lZcToC)r^d%O@)>ILk2sQlu(P|jJL!pt1kX#<*e608LwFnL1h!_}3a`Z+HCr8j?yh>QUsobLh z3=}})=)R=xn#>%_F4BAg3>-g zVZuKlCzRNXlt{J}F3*)cIRb|s$ z{lR94{UumBVU`!`pxMyc+B)~^>oFF`O(MbL5;Yq&pztT^eE?(=ZW;Uk5n{UVX(=r zPb!kShq& z2azq2CNZ9$4+eCo>aj|gWkE|z6zVP6E8kCz^;NG{0W*%Yyntywi;LrHe&Ty*G7gs| zKtGV8pA`&1h~eCuv%2{nd(jJP2c3WEpzX!O>y5!ZgR@UKq&RaFghp?G{tAwRHDuL` zMW%HRPGn>OE|kVxBk-PNib12pZYM3Pnzz|#3fX?Ng4ysZB zs*iOI53ru6Dq;M>$D$SI#$y&u!7J~^1S^VP5hKxoKzn!Kt+>Dzyk3a%r*K_yH- z79|lV&^Mt2H1kIec8qlQ(EA~8AUz5j#EpUmA&3z0qNAfJ&F`H80K9Bp*YYh`3N1z1hm9RWzSaPQUwC>JKI2ILhuMcVh-$@0)i}|_h5WN%KBcK z*ufKzHB!%`@ImVb$~(kBfoo7+VT!{iOsetq^OJqMk0a>TGZh{NBK<~%2e#g&;s#)Z zqaa+P=}0Vm%!aF9Bp*E(I_^%0%8bmc6rw#Ll&_qTJa7t(An($`dh z)gdUKjN^g815}O`Fm)Co%=7`Fce{PBCM=CkHfZq)ROW(7rxHKG7@>ktEpCAPo)E=x z>Xz0mh#$IjPyCnwkhLNR`b)#41W6nrq8KC$KFCDi;)0ryWsLnn*aV0JAkQ*g*hH_9mKLN^ZN6CKj5c^TG^wqoO^uiCEBJLlBO-)_>3ESh1o57Ss!mK{hA={v}uq z1y2D0P&7u46*s^H{lTCDj1}t@b=y}ImS`%B2+@iWt%w(@ptUk&I*)HcQ6>bhk26kQ zY@|AN=^6|BI+C>^@W~bv);(HqpCOl~MdXpUiLEDd_b{X_7G)kE z2#ChS@Nr&v5qG+|a2_o`RaUa&=P_n@AJ}dT$TC^r^E#rOT7`G+tb+#;Q^1QemBq|o zVfb*Y-qhb6*#`V41{`dJ2MHR~#0If*OPst3rGLlJ&|Tnd6awr)tPSvXz(<}V3(i68 z5%@e$(VHeb1dz2UfEa1WhA>ivykoM}jkn2V$w)Tg9vp_7<>{70RpshB4qiT4v#B`N z%rAtXL)bTPLqW=(9+aoCu`3_}Lb?A>tmHj{72Uv8r#lGw7|p;2G@v6kP^t69yVg!b zd=e`UAOq;$jx2*tV&%fjkKOVu^Wtvch(i|$S$-Hfv?p*N;OoX3_- z2vbJ>E#K5#x*Y_HH=vmN*=%188-gee6!!o@<;%o|>d5ctW2hB)q&uL8#{>_iYar&! zfUl=C(=Uo%6-fazuEQ4hDUMgjBH|tgi=%y*-5!^b89aE|e9ck5rgAtVOwOIR#dR`m zgwa*5&NLoIguo`favCv2k{nb42Wq<=UEe$-(IT52vCoUz z`52jsD*OyWbQNz?_LZ%wJ}WzrHW7!lbioIlt`ahC>-mXTB>BtRampZ}jD)eT^eZBv z!9@KcwJAq}%nVlc0-Ii#*#PZWpX^pK9ketc67Ci^vag(2w{Pa;W*^bKq^V>s3cB>r_B3%eOfh1-ms~~4Pize zWpKIju9GP+$kHzIZcpOTO<}fJEkD8=1qt^OVB|K+tes3wcJ~Q*Halsg%4|~a$#W)R zrVa)28x*3lAb3L)c@fVy_7X2|f0kX34#q;FX;aOMmVE(MjafvEhR?=$0tmh#g@760xIvl!rmH06S#p z#VgSVh9NTuVa_hG{)L%KaJ0efxK+2;zZGo+>-^tE+rl9)s)%PYMy(M#5(1WgIzYIN zuInK?TqzVTJMxWyt^%Z+q~2V*crgS_a}*MU%=GUCT<(;>KLs3KvhxKZZo}$99a&AH zDHO>VTqcHe3KkWdLlzeVpi3H5GB}Exjg~C!|53k{<&r%ptvD}>6a{3O;iC`&Zri|^#kcc1fT=alBky1${*bGD82!$LaSwVR@ z+cg9NB7j8Bwg2SFV1$!yeX=fyQ0heC-vMNr`jY`RXcn*eSN$fByqC7}1W87ZMOPqs z!ax$Hb{rBiI0BRv0JN}gF>_R}zQ$NLb_NphaFm{mgcl0CA_Pa2V0gMnoX_AnzbQZ z!Suq-s9NB=GHpd+PF+|Rd{=NT&Z1Zoso#)-{Ac|}M?B0nKHAN&bf*Z-O<QA5$n0kdSu_rg2bc=y|x2Vw|)?0kt6ak*Oo!6B8&e&%PkIBJwD53h>6HQLE7G zxPZYEq5vp2w}}Pe7jcR*z+i$0$OdSzrsQi@fAN}2#7;3c@I`ec-p7DVH*dLfdI z-@i!!dYDge>I01M5gK!}M$FVJfo+24tnT2j15F@Em5JU1)Ve5%37P<@l5V1*4K|oT zUgH}-!O0N}D;dqBTYO6?lC^58!H5|S|9Om4znL#2$WK>Cs?hAmWq;tcxaTLTMB zeR~8x6_|5Q8P;t|mzCRB&Ego;0>(hSN@%pQZ+*GTwxH1e3(gfIAB6m_Uw^5S^39^z z|LomITCv8Qtj^p;WtN+y7ghz81lUNrDIfG`D6tNseo53aM`dvUh1cB$PBA}ti(5z! zImpCC=tU3+`vez%A1ppN9{9c0&%4pS_fO!3FAf|@Y|NBLTdgRWc9`_E)HU1Z5;^|` zM=o}CrkP*(D@7jVaH{(TQxB%`f)5;RfKe+d77xuG5e| zp`pZPSQ=OQ2NCuHd9rbIta_SSbcm$#Rgv=i@QCyj(3?R_3_!}L{6!zK%-wxOAt5e(B6e!}TpW+d0t*00@om%s8Kf9MDSEKC&=T#B+bk z#FP{ZK_J5{%|X>8P&PF!uMGYDaI33M?pGHL*9QEIcSsB#Y|7!mK~-?v0l8IU?QYD0Fb3-ev#RnS4o#Q1uu2iBl_p$TpK4D%yKk_i8S=es|nD&sEx&_ zC!LYP4m2!YFPL}+*S{N%+zi6xnk*!3gSYd44>EKH)kUh zAB!J7yaFVj<0HX#Pv9w(0J8(@3rl~h51=IxV`D*AGX4IL{oCa)b3uK%JpECIe6*fD z6AQl@v|jveI=6>D!0Q=sYpAE**H=^T_W?U8gW5tHGSRd*MfdfO`HV{ARWHxC?nwH} zN+s^>mBk&GkufTNw+i5&_68xKp?6R4I7B<+e_u+I_FS0rf56<;*%>bDM4dSM);VSS zV(dfqhc(%ks+H$Q0+rvmUA#(AJ>=w#rw(@S)Ut82CSngUq>~kDs|4T|&#k!4yOLA_iyx^hcriY4c5qD!e zQv+Mrm3?wG%=Qsp4?OJqWIyt@W zjg6gMCxVt-U1;PGz?oAi6Q=Q%BNMd4Xa*lTs`>kCrg^OimxST|zL)Afx0nU5#IZ=33Udcl}sf zEs6k`78rBjwoK26>wD9p&;0ewJL|J~;^PMY?6=*O+U7J;oE1{?d%9_ZVqMY@6OZ(k z)9I`SJ^!}0w+eEFIfSEC6O^X1Z;V^g^s6GXqF+sSW5Cp1&o{jW*On`>9Di!*eDv1P ztHih6`XK!4?!>3%6gqhh~Z3pVV4$e7jOC5HGp$;uqO1RoMHrV=@ zIiS#k+yScD=CN++l)U6e3TIMjwP+3&xcu1#(2udb_wuZKL#Bc2q*Y^K@6?%KUeDQK zzly#+wIw6lqBo!E@T#`GVlOTsu_C$7BFvzLMt%?PwxYK}fjPlFbJSYU$xbdLH1sIG z!xTRu)L0J?m0`O=lgBDYfuAf z3co43T9?!^Z|x~B^XcA9n5gJ;?bA3yz&=)L#TK`)pQ5ToOFvuaDfRUi5SvN_%uxk0 zdX%Sm_3>BnRm$mtWL2o>x<`Mue@WBz+?Tt-^~R%mI#+*IxSt;z_tw+B84P_fbryAO z>gfu&xVWtBHp|3%8_Bs@$u#$0B{^&^Adm}{U(%SY%{-wR;B7wNNzE6S_r9s%gt=_S zAeWHhnM|DwDQFwnxG?#Ga71R$10y*Int#UYR8uL4IJ@TtLUb3>XID4v(kd+XONjB< zqw(Vo9U1HsaNG}DsIw{$wFa5(4B_P5{j-G%0brEb&W*0$0RT9U_jKK|(#uGRud~=_ zIlI_WfLN=Vx0K{(31cEPt_k(G?{7G1)!F- zpB_vboF5xGv-b6J)TT2<|o2Ep{{z5<^V_Q>0H7M0)5wpsoT z-^x>ka&=Jig{b^NC{z1eW0%KJ4OGYT`Ux`_}h0XHz zyWM|Nlr5{@kH~acx0!T}sS%8oAd(T<2koMDWzE;v@4S;%pBHv<=Si)Tzb|x1o_%Yf z29~#MwCz%>aZ9H&8F7j9s_BWAEe&)HNc1-8N1iKwv?$bj$9J44l(csIekQfuf>a+na6`av~Yco)>qB!?kQT$z5?8YAvTpvKpAfG0{RGguP z#NA{nKstTaQS$H#|Dl%KUvDkUH`wc<)hZQH{V7Q4*Fei7;CSxes1ct{D+inIs0paj zXv?G=hCUZ*giV*|bzJ}fl$x5ldVUUnTlqapOWt*m2W1w<51s4Xxph?^cH3d3_687} zh*s6sr(d_;u$lUKwZG5Ee|P2b@dW#G)l{^1ix3pJdxHniVzeqMDw4Kr*bb$=M*7LP zfdPfL+hr&rN#>8;4|=Dv zFFRNMzP3TOR%voSQ{X!t$w&of|82=l3)%)D;s8=wG_a z-12Ni(F$d)yL2LVA00JO!Jy@)j;akJf%Birm^#FYOSH1=xS=suhlKB!o>6)unoSW( z?t2JWh-RA1gQVlJ##(poUu87s;R#-|X1C|eGI7s^AiZ65=clHJHysWCfUd5M`^{qy zma={RE*x?)J(B!8*EThqVPYT~{0gf3i=b_ePCcKw$+Q;;7YbT) zXMM8-R3RY{0QvRg8&<2P=DmwBa(u@wr*=r43N(^~! 0 -> 2 -> 4. + results = g.get_shortest_paths(1, to=4, output="vpath") + + if len(results[0]) > 0: + # Number of edges is the number of nodes in the shortest path minus one. + print("Shortest distance is: ", len(results[0])-1) + else: + print("End node could not be reached") + +...and if the edges have distances or weights associated with them, we pass them in as an argument. Also note that we specify the output format as ``"epath"``, in order to receive the path as an edge list which we can use to calculate the distance. + +.. code-block:: python + + # Find the shortest path on a weighted graph + g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] + + # g.get_shortest_paths() returns a list of edge ID paths. + # In this case, results = [[1, 3, 5]]. + results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") + + if len(results[0]) > 0: + # Add up the weights across all edges on the shortest path. + distance = 0 + for e in results[0]: + distance += g.es[e]["weight"] + print("Shortest distance is: ", distance) + else: + print("End node could not be reached") + +.. figure:: ./figures/shortest_path.png + :alt: The visual representation of a weighted network for finding shortest paths + :align: center + + Graph ``g``, as seen in the examples. + + NOTE: Currently, the develop branch is bugged so that I can't display edge weights on the sample figure. I'll find some time to generate a graph from Cairo instead. + +.. TODO Update this figure + +- If you're wondering why :meth:`get_shortest_paths` returns a list of lists, it's becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. +- If you're interested in finding *all* shortest paths, check out :meth:`get_all_shortest_paths`. + + + From 2d22fc92dc8a8969303b72d3ff8372fded3cbd5e Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Mon, 22 Nov 2021 00:55:49 +1100 Subject: [PATCH 0517/1681] Remove america.gml Currently it's not being used in the tutorial, so I'll just remove it for now. --- doc/source/tutorial/assets/america.gml | 43 -------------------------- doc/source/tutorial/shortest_paths.rst | 4 +-- 2 files changed, 1 insertion(+), 46 deletions(-) delete mode 100644 doc/source/tutorial/assets/america.gml diff --git a/doc/source/tutorial/assets/america.gml b/doc/source/tutorial/assets/america.gml deleted file mode 100644 index acf490020..000000000 --- a/doc/source/tutorial/assets/america.gml +++ /dev/null @@ -1,43 +0,0 @@ -Creator "igraph version 0.10.0-dev+63a49e74 Sun Nov 21 16:02:47 2021" -Version 1 -graph -[ - directed 0 - title "Cities of America" - node - [ - id 0 - name "New York City" - population 8620000 - ] - node - [ - id 1 - name "San Francisco" - population 874000 - ] - node - [ - id 2 - name "Chicago" - population 2710000 - ] - edge - [ - source 1 - target 0 - distance 4160 - ] - edge - [ - source 2 - target 1 - distance 2960 - ] - edge - [ - source 2 - target 0 - distance 1180 - ] -] diff --git a/doc/source/tutorial/shortest_paths.rst b/doc/source/tutorial/shortest_paths.rst index 525b5e7fc..166ed6355 100644 --- a/doc/source/tutorial/shortest_paths.rst +++ b/doc/source/tutorial/shortest_paths.rst @@ -49,9 +49,7 @@ For finding the shortest path or distance between two nodes, we can use :meth:`g Graph ``g``, as seen in the examples. - NOTE: Currently, the develop branch is bugged so that I can't display edge weights on the sample figure. I'll find some time to generate a graph from Cairo instead. - -.. TODO Update this figure + TODO: Currently, the develop branch is bugged so that I can't display edge weights on the sample figure. I'll find some time to generate a graph from Cairo instead later. - If you're wondering why :meth:`get_shortest_paths` returns a list of lists, it's becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. - If you're interested in finding *all* shortest paths, check out :meth:`get_all_shortest_paths`. From faaf8333d98014d0f5012febf0dc861af08008cc Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Tue, 23 Nov 2021 03:11:50 +1100 Subject: [PATCH 0518/1681] Add maxflow.rst and bipartite_matching.rst --- .../bipartite_matching/assets/maxflow2.py | 39 ++++++++++++ .../bipartite_matching/bipartite_matching.rst | 60 ++++++++++++++++++ .../bipartite_matching/figures/maxflow2.png | Bin 0 -> 50120 bytes .../tutorials/maxflow/assets/maxflow.py | 30 +++++++++ .../tutorials/maxflow/figures/maxflow.png | Bin 0 -> 27217 bytes doc/source/tutorials/maxflow/maxflow.rst | 41 ++++++++++++ .../quickstart}/figures/america.png | Bin .../quickstart}/quickstart.rst | 0 .../shortest_paths}/figures/shortest_path.png | Bin .../shortest_paths}/shortest_paths.rst | 0 10 files changed, 170 insertions(+) create mode 100644 doc/source/tutorials/bipartite_matching/assets/maxflow2.py create mode 100644 doc/source/tutorials/bipartite_matching/bipartite_matching.rst create mode 100644 doc/source/tutorials/bipartite_matching/figures/maxflow2.png create mode 100644 doc/source/tutorials/maxflow/assets/maxflow.py create mode 100644 doc/source/tutorials/maxflow/figures/maxflow.png create mode 100644 doc/source/tutorials/maxflow/maxflow.rst rename doc/source/{tutorial => tutorials/quickstart}/figures/america.png (100%) rename doc/source/{tutorial => tutorials/quickstart}/quickstart.rst (100%) rename doc/source/{tutorial => tutorials/shortest_paths}/figures/shortest_path.png (100%) rename doc/source/{tutorial => tutorials/shortest_paths}/shortest_paths.rst (100%) diff --git a/doc/source/tutorials/bipartite_matching/assets/maxflow2.py b/doc/source/tutorials/bipartite_matching/assets/maxflow2.py new file mode 100644 index 000000000..ad8f36f6a --- /dev/null +++ b/doc/source/tutorials/bipartite_matching/assets/maxflow2.py @@ -0,0 +1,39 @@ +import igraph as ig +import matplotlib.pyplot as plt + +g = ig.Graph( + 9, + [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], + directed=True +) + +# Assign nodes 0-3 to one side, and the nodes 4-8 to the other side +for i in range(4): + g.vs[i]["type"] = True +for i in range(4, 9): + g.vs[i]["type"] = False + +# Add source and sink as nodes 9 and 10 +g.add_vertices(2) +g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side +g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other + +flow = g.maxflow(9, 10) +print("Maximal Matching is:", flow.value) + +# Manually set the position of source and sink to display nicely +layout = g.layout_bipartite() +layout[9] = (2, -1) +layout[10] = (2, 2) + +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout=layout, + vertex_size=0.4, + vertex_label=range(g.vcount()), + vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], + edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] +) +plt.show() diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst new file mode 100644 index 000000000..b5e6d44a6 --- /dev/null +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -0,0 +1,60 @@ +========================== +Maximal Bipartite Matching +========================== + +This example demonstrates how to visualise bipartite matching using max flow. + +.. code-block:: python + + # Generate the graph + g = ig.Graph( + 9, + [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], + directed=True + ) + + # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side + for i in range(4): + g.vs[i]["type"] = True + for i in range(4, 9): + g.vs[i]["type"] = False + + g.add_vertices(2) + g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side + g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other + + flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1 + print("Maximal Matching is:", flow.value) + +And to display the flow graph nicely, with the matchings added + +.. code-block:: python + + # Manually set the position of source and sink to display nicely + layout = g.layout_bipartite() + layout[9] = (2, -1) + layout[10] = (2, 2) + + fig, ax = plt.subplots() + ig.plot( + g, + target=ax, + layout=layout, + vertex_size=0.4, + vertex_label=range(g.vcount()), + vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], + edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] + ) + plt.show() + +The received output is: + +.. code-block:: + + Maximal Matching is: 4.0 + +.. figure:: ./figures/maxflow2.png + :alt: The visual representation of maximal bipartite matching + :align: center + + Maximal Bipartite Matching \ No newline at end of file diff --git a/doc/source/tutorials/bipartite_matching/figures/maxflow2.png b/doc/source/tutorials/bipartite_matching/figures/maxflow2.png new file mode 100644 index 0000000000000000000000000000000000000000..86a6fb1bbbcdfc709431df4f216d7f8cd1433c27 GIT binary patch literal 50120 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
  • v1rtwQuh(6Rat3QXuUpL9ee_v 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 I?D>cO2NEF>V*mgE literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/maxflow/assets/maxflow.py b/doc/source/tutorials/maxflow/assets/maxflow.py new file mode 100644 index 000000000..b28b81bfc --- /dev/null +++ b/doc/source/tutorials/maxflow/assets/maxflow.py @@ -0,0 +1,30 @@ +import igraph as ig +import matplotlib.pyplot as plt + +g = ig.Graph( + 6, + [(3, 2), (3, 4), (2, 1), (4,1), (4, 5), (1, 0), (5, 0)], + directed=True +) +g.es["capacity"] = [7, 8, 1, 2, 3, 4, 5] # capacity of each edge + +# Runs max flow, and returns a Flow object +flow = g.maxflow(3, 0, capacity=g.es["capacity"]) + +print("Max flow:", flow.value) +print("Edge assignments:", flow.flow) + +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout="circle", + vertex_label=range(g.vcount()), + vertex_color="lightblue" +) +plt.show() + +# Output: +# Max flow: 6.0 +# Edge assignments [1.0, 5.0, 1.0, 2.0, 3.0, 3.0, 3.0] + diff --git a/doc/source/tutorials/maxflow/figures/maxflow.png b/doc/source/tutorials/maxflow/figures/maxflow.png new file mode 100644 index 0000000000000000000000000000000000000000..8cd123fe71d00501773db7b412696f4f462893e1 GIT binary patch literal 27217 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 bu^;#T Date: Tue, 23 Nov 2021 16:29:12 +1100 Subject: [PATCH 0519/1681] Fix Typos in Maximum Bipartite Matching --- .../tutorials/bipartite_matching/bipartite_matching.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index b5e6d44a6..4dcef0291 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -1,5 +1,5 @@ ========================== -Maximal Bipartite Matching +Maximum Bipartite Matching ========================== This example demonstrates how to visualise bipartite matching using max flow. @@ -24,7 +24,7 @@ This example demonstrates how to visualise bipartite matching using max flow. g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1 - print("Maximal Matching is:", flow.value) + print("Size of the Maximum Matching is:", flow.value) And to display the flow graph nicely, with the matchings added From 588ddbaf36f4440e6f9fcd53c12bea3af7083966 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Tue, 23 Nov 2021 17:27:16 +1100 Subject: [PATCH 0520/1681] Update tutorial to use maximum_bipartite_matching instead of max flow The max flow tutorial has been moved into the "archived" folder, in case we want to use it later. --- .../bipartite_matching/assets/maxflow2.py | 2 +- .../bipartite_matching/bipartite_matching.rst | 60 ++++++++++++++++++ .../bipartite_matching/figures/maxflow2.png | Bin .../assets/bipartite_mathing.py | 45 +++++++++++++ .../bipartite_matching/bipartite_matching.rst | 50 ++++++++++----- .../bipartite_matching/figures/bipartite.png | Bin 0 -> 33545 bytes 6 files changed, 139 insertions(+), 18 deletions(-) rename doc/source/tutorials/{ => archived}/bipartite_matching/assets/maxflow2.py (94%) create mode 100644 doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst rename doc/source/tutorials/{ => archived}/bipartite_matching/figures/maxflow2.png (100%) create mode 100644 doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py create mode 100644 doc/source/tutorials/bipartite_matching/figures/bipartite.png diff --git a/doc/source/tutorials/bipartite_matching/assets/maxflow2.py b/doc/source/tutorials/archived/bipartite_matching/assets/maxflow2.py similarity index 94% rename from doc/source/tutorials/bipartite_matching/assets/maxflow2.py rename to doc/source/tutorials/archived/bipartite_matching/assets/maxflow2.py index ad8f36f6a..844d12306 100644 --- a/doc/source/tutorials/bipartite_matching/assets/maxflow2.py +++ b/doc/source/tutorials/archived/bipartite_matching/assets/maxflow2.py @@ -19,7 +19,7 @@ g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other flow = g.maxflow(9, 10) -print("Maximal Matching is:", flow.value) +print("Size of the Maximal Matching is:", flow.value) # Manually set the position of source and sink to display nicely layout = g.layout_bipartite() diff --git a/doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst new file mode 100644 index 000000000..4dcef0291 --- /dev/null +++ b/doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst @@ -0,0 +1,60 @@ +========================== +Maximum Bipartite Matching +========================== + +This example demonstrates how to visualise bipartite matching using max flow. + +.. code-block:: python + + # Generate the graph + g = ig.Graph( + 9, + [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], + directed=True + ) + + # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side + for i in range(4): + g.vs[i]["type"] = True + for i in range(4, 9): + g.vs[i]["type"] = False + + g.add_vertices(2) + g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side + g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other + + flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1 + print("Size of the Maximum Matching is:", flow.value) + +And to display the flow graph nicely, with the matchings added + +.. code-block:: python + + # Manually set the position of source and sink to display nicely + layout = g.layout_bipartite() + layout[9] = (2, -1) + layout[10] = (2, 2) + + fig, ax = plt.subplots() + ig.plot( + g, + target=ax, + layout=layout, + vertex_size=0.4, + vertex_label=range(g.vcount()), + vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], + edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] + ) + plt.show() + +The received output is: + +.. code-block:: + + Maximal Matching is: 4.0 + +.. figure:: ./figures/maxflow2.png + :alt: The visual representation of maximal bipartite matching + :align: center + + Maximal Bipartite Matching \ No newline at end of file diff --git a/doc/source/tutorials/bipartite_matching/figures/maxflow2.png b/doc/source/tutorials/archived/bipartite_matching/figures/maxflow2.png similarity index 100% rename from doc/source/tutorials/bipartite_matching/figures/maxflow2.png rename to doc/source/tutorials/archived/bipartite_matching/figures/maxflow2.png diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py new file mode 100644 index 000000000..7a1f95fcb --- /dev/null +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py @@ -0,0 +1,45 @@ +import igraph as ig +import matplotlib.pyplot as plt + +g = ig.Graph( + 9, + [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] +) + +# Assign nodes 0-4 to one side, and the nodes 5-8 to the other side +for i in range(5): + g.vs[i]["type"] = True +for i in range(5, 9): + g.vs[i]["type"] = False + + +matching = g.maximum_bipartite_matching() + +# Print pairings for each node on one side +matching_size = 0 +print("Matching is:") +for i in range(5): + print(f"{i} - {matching.match_of(i)}") + if matching.match_of(i): + matching_size += 1 +print("Size of Maximum Matching is:", matching_size) + +fig, ax = plt.subplots(figsize=(7, 3)) +ig.plot( + g, + target=ax, + layout=g.layout_bipartite(), + vertex_size=0.4, + vertex_label=range(g.vcount()), + vertex_color="lightblue", + edge_width=[2.5 if e.target==matching.match_of(e.source) else 1.0 for e in g.es] +) +plt.show() + +# Matching is: +# 0 - 5 +# 1 - 7 +# 2 - 8 +# 3 - None +# 4 - 6 +# Size of Maximum Matching is: 4 diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index 4dcef0291..c8a2339f0 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -2,31 +2,41 @@ Maximum Bipartite Matching ========================== -This example demonstrates how to visualise bipartite matching using max flow. +This example demonstrates how to find and visualise a maximum biparite matching. First construct a bipartite graph .. code-block:: python - # Generate the graph + import igraph as ig + import matplotlib.pyplot as plt + g = ig.Graph( 9, - [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], - directed=True + [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) - # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side - for i in range(4): + # Assign nodes 0-4 to one side, and the nodes 5-8 to the other side + for i in range(5): g.vs[i]["type"] = True - for i in range(4, 9): + for i in range(5, 9): g.vs[i]["type"] = False - g.add_vertices(2) - g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side - g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other +Then run the maximum matching, + +.. code-block:: python + + matching = g.maximum_bipartite_matching() + + # Print pairings for each node on one side + matching_size = 0 + print("Matching is:") + for i in range(5): + print(f"{i} - {matching.match_of(i)}") + if matching.match_of(i): + matching_size += 1 + print("Size of Maximum Matching is:", matching_size) - flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1 - print("Size of the Maximum Matching is:", flow.value) -And to display the flow graph nicely, with the matchings added +And finally display the bipartite graph with matchings highlighted. .. code-block:: python @@ -47,14 +57,20 @@ And to display the flow graph nicely, with the matchings added ) plt.show() -The received output is: +The received output is .. code-block:: - Maximal Matching is: 4.0 + Matching is: + 0 - 5 + 1 - 7 + 2 - 8 + 3 - None + 4 - 6 + Size of Maximum Matching is: 4 -.. figure:: ./figures/maxflow2.png +.. figure:: ./figures/bipartite.png :alt: The visual representation of maximal bipartite matching :align: center - Maximal Bipartite Matching \ No newline at end of file + Maximum Bipartite Matching \ No newline at end of file diff --git a/doc/source/tutorials/bipartite_matching/figures/bipartite.png b/doc/source/tutorials/bipartite_matching/figures/bipartite.png new file mode 100644 index 0000000000000000000000000000000000000000..3565aba619105e82297afe4aa60406949e319645 GIT binary patch literal 33545 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 literal 0 HcmV?d00001 From 993e913b4603aad1502a9232642673b1c5e198bc Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Tue, 23 Nov 2021 17:41:40 +1100 Subject: [PATCH 0521/1681] Update shortest_paths format to be more in line with recent tutorials - Add a short summary of the example at the beginning - Remove full stops from comments - Included output of print statements at the end - Made the tone _slightly_ more formal (library is aimed at researchers doing graph analysis) --- .../shortest_paths/shortest_paths.rst | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index 166ed6355..21834b3d1 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -2,57 +2,66 @@ Shortest Paths ============== -For finding the shortest path or distance between two nodes, we can use :meth:`get_shortest_paths()`. If we're only interested in counting the unweighted distance, then we use: +This example will demonstrate how to find the shortest distance between two vertices on a weighted and unweighted graph. + +To find the shortest path or distance between two nodes, we can use :meth:`get_shortest_paths()`. If we're only interested in counting the unweighted distance, then .. code-block:: python + import igraph as ig + import matplotlib.pyplot as plt + # Find the shortest path on an unweighted graph g = ig.Graph( 6, [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] ) - # g.get_shortest_paths() returns a list of vertex ID paths. - # In this case, results = [[1, 0, 2, 4]], to indicate that the shortest - # path goes through nodes 1 -> 0 -> 2 -> 4. - results = g.get_shortest_paths(1, to=4, output="vpath") + # g.get_shortest_paths() returns a list of vertex ID paths + results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] if len(results[0]) > 0: - # Number of edges is the number of nodes in the shortest path minus one. + # The distance is the number of vertices in the shortest path minus one. print("Shortest distance is: ", len(results[0])-1) else: - print("End node could not be reached") + print("End node could not be reached!") -...and if the edges have distances or weights associated with them, we pass them in as an argument. Also note that we specify the output format as ``"epath"``, in order to receive the path as an edge list which we can use to calculate the distance. +If the edges have associated distances or weights, we pass them in as an argument. Note that we specify the output format as ``"epath"``, in order to receive the path as an edge list. This is used to calculate the length of the path. .. code-block:: python # Find the shortest path on a weighted graph g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] - # g.get_shortest_paths() returns a list of edge ID paths. - # In this case, results = [[1, 3, 5]]. - results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") + # g.get_shortest_paths() returns a list of edge ID paths + results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] if len(results[0]) > 0: - # Add up the weights across all edges on the shortest path. + # Add up the weights across all edges on the shortest path distance = 0 for e in results[0]: distance += g.es[e]["weight"] - print("Shortest distance is: ", distance) + print("Shortest weighted distance is: ", distance) else: - print("End node could not be reached") + print("End node could not be reached!") + +The output of these these two shortest paths are: + +.. code-block:: + + Shortest distance is: 3 + Shortest weighted distance is: 8 .. figure:: ./figures/shortest_path.png :alt: The visual representation of a weighted network for finding shortest paths :align: center - Graph ``g``, as seen in the examples. + The graph `g` - TODO: Currently, the develop branch is bugged so that I can't display edge weights on the sample figure. I'll find some time to generate a graph from Cairo instead later. +.. TODO: Add in edge weights when possible! Matplotlib does not support displaying edge weights (and the develop branch implementation is bugged). -- If you're wondering why :meth:`get_shortest_paths` returns a list of lists, it's becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. -- If you're interested in finding *all* shortest paths, check out :meth:`get_all_shortest_paths`. +- Note that :meth:`get_shortest_paths` returns a list of lists becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. +- If you're interested in finding *all* shortest paths, take a look at :meth:`get_all_shortest_paths`. From dce50b337cc5d0adf4e509106c1d29c7fd566a27 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Tue, 23 Nov 2021 19:08:40 +1100 Subject: [PATCH 0522/1681] Update quickstart.rst example to be more neutral --- .../tutorials/quickstart/assets/quickstart.py | 44 +++++++++++++ .../tutorials/quickstart/figures/america.png | Bin 16021 -> 0 bytes .../quickstart/figures/social_network.png | Bin 0 -> 26448 bytes .../tutorials/quickstart/quickstart.rst | 59 ++++++++++-------- 4 files changed, 78 insertions(+), 25 deletions(-) create mode 100644 doc/source/tutorials/quickstart/assets/quickstart.py delete mode 100644 doc/source/tutorials/quickstart/figures/america.png create mode 100644 doc/source/tutorials/quickstart/figures/social_network.png diff --git a/doc/source/tutorials/quickstart/assets/quickstart.py b/doc/source/tutorials/quickstart/assets/quickstart.py new file mode 100644 index 000000000..2db46d73b --- /dev/null +++ b/doc/source/tutorials/quickstart/assets/quickstart.py @@ -0,0 +1,44 @@ +import igraph as ig +import matplotlib.pyplot as plt + +# Construct a graph with 3 vertices +n_vertices = 3 +edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)] +g = ig.Graph(n_vertices, edges) + +# Set attributes for the graph, nodes, and edges +g["title"] = "Small Social Network" +g.vs["name"] = ["Daniel Morillas", "Kathy Archer", "Kyle Ding", "Joshua Walton", "Jana Hoyer"] +g.vs["gender"] = ["M", "F", "F", "M", "F"] +g.es["married"] = [False, False, False, False, False, False, False, True] + +# Set individual attributes +g.vs[1]["name"] = "Kathy Morillas" +g.es[0]["married"] = True + +# Plot in matplotlib +# Note that attributes can be set globally (e.g. vertex_size), or set individually using arrays (e.g. vertex_color) +fig, ax = plt.subplots(figsize=(5,5)) +ig.plot( + g, + target=ax, + layout="circle", # print nodes in a circular layout + vertex_size=0.1, + vertex_color=["lightblue" if gender == "M" else "pink" for gender in g.vs["gender"]], + vertex_frame_width=2.0, + vertex_frame_color="white", + vertex_label=g.vs["name"], + vertex_label_size=7.0, + edge_width=[2 if married else 1 for married in g.es["married"]], +) + +plt.show() + +# Save the graph as an image file +fig.savefig('social_network.png') +fig.savefig('social_network.jpg') +fig.savefig('social_network.pdf') + +# Export and import a graph as a GML file. +g.save("social_network.gml") +g = ig.load("social_network.gml") diff --git a/doc/source/tutorials/quickstart/figures/america.png b/doc/source/tutorials/quickstart/figures/america.png deleted file mode 100644 index d86d613dbda293553b139a717c14b4d5d6a4dc3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16021 zcmeHuc|4cv`tB==CPUFkWhz5x6(us0u*@PAQc+0Cn0ZK=(U(#hC_{z}k<7!QA}R_Y zV;M3PnP=yEwD;QkcYee9=bZn}{(RQnd-=ZaGu+Q}-}iN2*Zp4A(NbNxY{N2&qE@OM zP}ZX;x)JgZ(-Qop;&baY{11bJqM88{{^P-P@(TWa>A3^O4iv>=PX3{blY44~Kkju@ zIpnBsXX)r-Zf`-^nme95W9N9r>i8CC3wsAEyR%z%iS7{HwSCKJN5^wgVq*V#K-A9u zl-R@NiBS}_g;G;iFu3rbzuDE%U~%~Ty!J9CfB#fojiM`Mo0wQxnapp!zvQXiYgqI$ zCL_bv_J^HVyJWRY^{Jf7%JoN%y!gwId)#mpoiW3ihA`GKF6G_EB|J8Vlnw}uPV*{h zKE1e%R<3!rCB;Fqv_?PPO>b}4`+SZB#Tld=snePnqva8VTFmTroYq!bvb zB0451iei?pqNC1He$12)J*y){ar#8kQ*G4$fAs&o1;iE?&d(0iCYwmVe)UQ~R8&hf zkTZLrac10m-bF=4MR50S!{^VRyY(eysvoPCp6*Yt^mCi0e5Q>WelC7+wmHY?aJGGn zbASC~p@Rtm!onI~zkcPnZ!DY}O&R@mUCgU5FEUbIv~G6(p>2Jt`MBps%BSrclT6u% z2YaL3MtkGcZwt|);sGA%$*42c|MSlqQ{w|E zeRV0VqqDV1M~uV@CY~HWe%!IG$k)9fh@O!-&aBX@xBhulj`NR%iZC&osgWMH9~pIg zYCSzY*442$rJVcYzLs3ppe38;3k8)%F>^`9ohS_~FDnb?o9)r|)|(#t!LfQZZ%Uo} z<{%!b&3*FxyvL;1{OD?z8HdhyMss7fM2%DPqDr{~4HKDJkcdfazaJI2<^1is$w*f4%5cAHxzrTl3 z*5zyQCEq@itaD!<{fx4yOP+gueJutkobRnooI8?vc2aI`;>3JnNN>4l3+s~{OJvz9@uBFL) zsN?M_*DN~f_4)FAzNUR5?`_;lbyuo2dW$4A35(1RMJ{2UJFv@)w&cwLtJayzb9aW~ zJ!B)&($ZeowaQA^elcKUW81Lfm=9mPMwqCS+ekN;-L%%zrjp{~ks5vN#j;u&8r7|H zW0|=*LBdAK-QQkc-ynWEm`{+0=SjWio6=GnEWkdudBZNt8+=v`X%ANxZAFUQ!;^(3f=B!x=hGOXvIX)x_Hx-R&$0uUm2qX ztMy)Zl~~q3GuA)x=7#Rsv&nq%c-gly$snHQA=_%wYBbtgOF!>9F=(!NZ>I`wjo9<) z3-hz%8RjKRL=L6OpFe;8OTPQK$-okdY11PmmbBTY(J6(3Gj@e$UmO&z4UKZbc~$(4 zj82}6CN?*ODyT=_j$68s# ztZt*VMjUI_#PVz0VZT>6_QgiZu{#(+cYh#fJREDbuW&B$k^9fRaPK(17zZJ1IY*W z{}!<;d~gYETMJC~P18{mkD1)o2Zx0{ogbf{i}3n&-RyMZOIv>~>Rcs$NIO-zX|~s> z@RM*#-jk#wnX86>KM!W?rzO%v%?da#)Mj|sytF;CKHzaymgFUW z|Inf*6!ny++u~JAzPm+u?8`F^dS^c8IN7-F+vN1)%k5Np>gSb6#*grv&f!Uecs>5w zc>V9mvZn@G^3%3!+}~p-y4wZ@Tv4KvdA6BJnu=ud+oY^5H~Ru7%g(_O69gZ7Bz z;JH&%|Nd@Uq1`qOX)*gY1#R*i(Jf}Cf|aW-_ImkwHO|dWTNvNjeWqUL%vc$3P}z3;=^0NN-}#(kW_ITD6P4xI!y_4Y>@_Zu$&K~)$tpYP3xko{ zD?a5o^<8@(j+K?{VBfr1EbO}XEhdU%sRGNBV+^w|l`b>CP1|l^X}RV;8{WNucNg#K z&g8=Vx?>^8%Ntd{T}tY3fVluSw{I%FPYK(|7NMJwtRCy#94;u6VhO!D_< zIGA7{80St={R&v1_(iYAk5Q_fqiz|9s>jXkK0mwHbT$)lQmLM<$Q-jYu#Gh0c5SUl z`?+o`R`Y)O59dU?)&d+Nr(Y!Ft%nMQKkA>KP}kI~O*oinprfO6at#LuKc@1Uox(~5 zzLQ;E!b-lqTI!l_nshWf?NB{}9uFQV-Q<1)rrN85m26);`|{bd?J=>j){{e>uZvr< z&Y$4t=eH{Ip~GC|>v297e{^R)+~=wnA0L1CPkoc}ak}y85RgU;3reROopInJeW=u({`y=S1D8`9mTe`HFW|E5fW3a(e$d3~x#j;A{-igD8gd9lKoG|P8)A9E`O zurHm?cKAN>L3-fyr)afsId54quuo*Wv;^joo1d(dIPqr1N-n7vW$|MU-#_#;yR_E6 zy1H7zu4NzY;BE{lymEthm4;Y;G*)nWV(_a0{2nzNzHT&rJ_q*cUG2{_<=~o>hA+rSk`?V^bK($4}*E9hZd{#kR(;(bW6c)+e+VP(iJr@NvSIX9*j0QRK6A0yt#f??1B39^mHT> zasF-FY`Wgv!(sdRL`Er-+%GrzR{Lal?3mR8 zY>o^B7>puwlw-Wjas>zK#mowxvB=Hnz!^kDH20isO2q-3TjCcOi5q&yYo>j3McQ`F zyE_!j&CPuqZVMZYd{o{1cE<|bPLo2fW~?*PgST6N16inMSLHt5inP$XcjJ(J87+-c zOlwDasylyOK+KqtXW)tvLfDs^`WS>|Hk}>kDml9@;Lqxs44b+$uzLAgKjtIK`}bSp zcoB92%yHuQDU?RzDcOPlR;;IM6J>R*PuPERL%8JmC)+D%rR};GE?hX~JUiudFIKQ* zhd6G4NxoYxS=AY=YPEGu+~bZ8M3fl(w~DedgJoTc84KrI?Fd;DqvWH{u~&inOj6~c zSFSK28sa&%kgf5Uz_;P%#yuUQZdg3oFnn`@k%7xX@4|&Fcsv$etu1G=z2C~pYUc{2 z&0Z6a5qZy$Gv3YW7-|Is2REWw7-O$q(Pd?2MccP+Q-yPohdl;;)6&z)J?mGg(iJsG zF*SjwV8@mFhHqGZezyK@bDpdIu2UaEOWQv(ZuW=-UTDY?r0Oq5Mjc(ce0jD1($z5F zBhN8oL=oW~xm;t|oaRF*1oAi?Y$hf(bq7|jB`7EuzR%VEd1~b&nIoHmRJ~{W%$#sD zH||;N^GTsfH~G?~OIH2$&k?jMWyT*^*Clh^?du{Ig-o3^H1=GnuSEq=z(wL+;3Gmpk*FG9XT`rav3IAAZ2|Nf7FW9Q} zAkL-5RrOTmqYQFX>lwHL*KF93z{b1(^qU*&ry44~Vy8y?7|HX8Q;+#>)eMi$Z1uXI zN-t2yBqiiuRi(3X{ssY>^;e?WkB zch%!?S`Z`UbuPk(A&0c>o{_t%%19jPua zFV{0L2$coO%&q&2tE02is^?RTx`u}Enn56QE1+v!1-+>KH_v=wrG7Fiu<-bqdUbsN zZbKf^)D&6c99F@>ZjM=9Tg2*vSM9ibTprY)u&gn zE9u%kAXTu93<=B0nE|XDSz9M)J(N0lvdccZrqJ6P$v{v@s2mSQ3Lri+H1Nw`sB81I&nnQ{jHI$WBCRZd?=>N7m=~eF)R0Ti!&< z{?%T3{oUEi%|D!lrKG+*T1;mkiTuKpgN4ajoIF|9H`?3%F-pafo10txz=2@%b#C2{ zFZUa>%3nh6y%K4}xp?tnQfpWkzb+OPWF6~ze?M%_o)YpecNU{0eX-C8S@~Un6p+X^qcnHpfVY?4zRme0&13vZj5XlR~V*Rf-?o zyBCi)NBbM}Hw9hB4f=zDi)m3H|XO~Crfz-4rY~^>W=FDoOB_P z`O%cE4}6a5z;_au>cxptW~c5d1*B?Zy_C*Pf9BnhR{!)Pb)YFby?*=Ft@~jy)Y%x} zxw$mu%-gM?T*bB`*ZBv{D*OxPcy5gP6!9E@XIn9|!47iyr2MRK3J8E$zVL1vn2#0` zQi~nq0oH{F)Jw)MHSbvQ`t@rz=c2C*cRDl{_CW%*1<0*`hO-GO{UL z<{JCiEL@TH0s-%M`sbu06+2eoshg?v%u3`YmuO)jAvi#Qw&%vUJr?T%aI{F#m$#5b z>#pfx`n51<+EI6Nl1~aGR7EA!tcsv~;iU8^UhLBYH zPIltNfoTY3O2{bp?%jKBi849NeBHI6ps-V+~3 zRa49yx_V*fXNdE9m_WLTTs@MfwhSfRp#)4^eA93!r=VKl7o_o&v?{N7oV#xv~ zWZ>%j_}`&r-<<)(QmU??J^mm-_i#o6fG$c00Nn!5=~b9z!4_W`N`p3&;AWV7G~Un8 zk0%`k8G>8S^U_Vc{XXv(4n*?p)z#ZV!^0C$4ze$I580~O;heiv`WP>Xd|Xvs60(#PCbIIocZ7ANaG2TbPaj$=Y>jd}4xT*Npn8e{A} z6Zh(sGSK9eRC95$9c#3+`S$86NqgXsEZSpz(!u*F{3@bnVJMb$?5^Tb?SLm$ zOAq8lA%j`X%}n}p3D7Zc$?FinES5h)i`9-u2B7CyKAk&IkO&)Ex8%9{b7isMq4?8P zk-lDI=@C{)v>RkSF5F6ukn=XxkNs2E-ad`w{HCU(KoaHK{FoyRYEiHeK8b+Z;3ZDMBVDcP4pfV}3|Evye z{2{t#3?&P^{9bH4I4+<;ogDIzARfYPM>4>&06Kje{v%-4<;He952NrBM}>vx`(L2o z6jMN}Em`1@Rj&XA@4GB}{@W`a^Fr(pOJ7k^1qkodSzG3QMd%jPF+bY(Iw+7V_^wv{Bg%B+-<7o>Gex8C@ccZyGvN)YC0>z0DK9K z^NG52`LeK}pxPaao_nVr>7G1!^4Oh_kf@OnM-=@45%Q>CcAs6=v>EqHG0rqU2ew7b z8)@G>e+b3Kig&20;sMFZ+kc(@^1>$`AnEWy-u*Y2 z{PTfFe2q;_A9Ynmq`+r;AG)-BL_i>RRj#6IlO|C1)fKkzKTZS?|BXkISa^QT_Ct{b z%Kpe~-P)hZp}^>q%L*)w0ANL!-kzASYnMF1rjpufZsxmOANXSwNCpNBT+~m6)2HJC zn7I;2?cNz?_L-h#AM4#tfYgsqRLBc0ZhayMXm8$V>VEY9O@B$&qL4@eO92kNr#(pS zr8^g;K=>_CyFwx&Nr#Gr4<@2O zmTtmBHtEoFMKB~l@j^QtvXA!Gr9_~<3*T*PT^(?4*+Tv(vIQhgG)$t^_0_`O*9aD$ z8vBumEFv?}c5%1um7zeAMi>VxLHHV`n2J=SUArU$3g0BQnxjEP*|iLfZYz6}rOy4Vrs6nFrr;4;f* z=6Abx?hL(k>+!_I1-Kg^M17V>Ii{v8t^i0YD*6G%W}I!7Ld-}hoRiv7`R*Mdncc?v z1%pbJ>e-++MXs=i!DQgq6DNw#C=Wd7FcWfyFIpFqQUr*;E5@KS2NBZ z3Ehe{bymp*cmjf9EoPph2agrF4qMu`eKi$W^fec7xk1!yQP>fjn-`KbQxe_wZ_|InFNdRSnonOCsQw|*2U;puqtu75= zR#FG%lyd5guCG7zKVao%x(g0>wtRbUOoj4GyZj6V?py%F(1QqAKXHO%)vDtq&RJ#) zuIH0>8i0GJ=5U}nc*}e|ffN z)hmo^hvUK&*0t)G0<{)zzr~7wOo8el0+kquuIukXZG!iy$#-`tHr>B~)u;!@=I3T{ z@IYw-yWKl(sUitOZc(VW-M=uyIod@=mh!aim^``unacGwxDhh$V|@L)e@6w$kErpG zo_Frrb^YHd<_e1vXO6WCYbbhKFgIm`MIzmz=ATo$pFDZu2(u34eL$%#0T?DTGg#95Ddzf{ zpv@`irz)+HiQK+Fbm3Ejfw`f$# zPCvUNX69w2@C>y9Vlt9#{_%7Kqr*5p!=pzZ;DtZ9WZXzn)8qsYL8@YG%0ddea<@U&A2ttcm6W$v@5+w-ziDm;OXiTwSCWz_S*YeD;T(cXf*Q ztPSSYitwJxHofq5F&tKC*Dig;Zsw!jzrMW=-(gl(JB#|6SOH}cTp|?i z-yg95#=6IddHF--!cwk7(R_l4$S562Kp5>!OtD5vBt>mo*=&T&c#}f{M3gsyT*7Kn z-g6#p&U|m%f3Z)-6gJl7{`gh8xeh0AMCRA4)%=53IKsuPXe7yu_8ugx%*7=ur#|!< zi9UB#vt4qIEK}YrK%0uX{A5pVIdH^?L2= zzz;FSF3+aFR}Z?J0F3QzNPikGVG|4ANr3#=>gpK??}-tD=|~k){P+Z2%#kbQ&vw7e}@Vyh~mDr zHV}UZi7s+~?VjJlQ)xAyK0g^TuM0VxXqz1ONm+_=5}c231u#rhO2Je@*(msR-Hx^M zJiNSwvB{sC>T7k+G2uaL%6+bUkM`sg>2IZC-Tn9G-CJZNBo2B{R+tgG2SVy;=vK64 zLD!(MFM3+v_`+&9zw*RDFQzEZT<@z~(lkn`7f7_TonqdC1g8=4 zTioXJb`s%GsXc^5JX>V8;j9LtmN7Lx23HPTfcW(M+={EeD>Ptz4hU@nA(%ORYoPC#>snmlv6K*>Vze&n1 z@H%<=^?|xbg^P};g@78rB`2Hxv5A9{zc+f|$GL~Omm@RdR+*Cd4%FIRS zBtS%lmbv-Dqbze}35mcsH=~Fng>G@-pX1iN8!Z=sUkGrZxd*t+4Gh8Pn~ip$L6=xI zZ>amsn^Dp^bthAg<<^K!Qg3fVH1j~_oQPy6aV0;$MG!a3)1DOxM;wHHAYoloh0w#k zVS|v-fh)&1C?_0jW_p<8rr+9BcCer_MLn5Sf{Eo|$OiL-!&gsK4Unj>psS-3iTs63 z$hFsASMp2no|J~ovUNFbdNu@5q^h^M$ZYcf)^r3)IMQT+I+H~7s-4GtO z!UagsjS9$LF2xQE6Ur1(`4ljekh!RBORRi-Rg;*b5 zJieolYv{_AYriVr^^*tWu2g=nhV}&U#R)2?+NBg#S=3ZF*ZsjWxI0>a3-R-uHUZkt z4m}2|V_{*TCbVMm?jI+_Jtod6o+Ys+OBjg#Hz{u2=2^KejYfnQNuI*l-EpaFv5}^aKZ2je#`cc%6lZQvf)a)Zl87X? zY6#2~J$0?DqM{m->216YV&MQ*kafOY{A3xcqUnB&?-N}dEM}ah460zN>%JYI8Vvn; z;#d19T0P)Kp*4gJuSg< zxG;E3@h2-L;p_#~P9CuQ@L)ZYE21Ki_@I!m*I_X zvHk|%V2ITR)YR0_{y}l4Y?%-R{&s>yB3}l*a=RMIa$rx0(4lDh*I@G!ANwwF?L;z? zogL9P$+lOa)kLeUhPoj$R(~P^O)c*?7Bgcnf4=SAv~amgu0?LP7a{ z;}dctj-`{{AJomYd9K5Yxb0fMi?_Tu9RXlnlz2X(@q zbKDgUqE>I~TUmh20^p5O|^Us}Fa2*8t8gBB9( z9;FtWeW(CN;P1yR@C|OomBZdbLP9v!u2n5#V~5?<%aCix1ra&?LV$JXrEMdOsu&oF zJ)esJFOs4yJE;tTMi0aWUp(+F;nbV5cX6x6;~EdrqrB20Wa)J4{3a$>)Gja!fFILd zbwXsgJC5}m$2@)7cU`OiOGg<>Uax=7%Y9h{mT!sy`UT~nY3A|k($MfQzZ#dc^9p~? z)YW)*;C$D+^?26?5Y0EZ6jc^1iV=3(F|s3(4T@+*$R_=!ew7hcX#b{%bp7yog5hUE zATSCey+Nqu?-1BS{*(gZNeF*JX{o<})PYe@$N-OJxJtL>B1CYQ8XDfw)7Q`Nm>iNX zL!l{DvV&=F`AX1I#h@by+i;CTcoDZZP5=VI;motP!_GzdI7p!f?WI9vKHQZR{(1i@ zn=vwRNTC~G7vA=bLRURFG(Ag;MhOk>FylT=qrg&5ZVvx(B(~TWtg-LmMqWzyDdjuXoV}pICU}W6f7zH}6Nc9zXTfzn2HXB3uY=7XuW!b^Y3%A)tA+OWfYy zu5J_enz^9J+yth6OC0&#wsC8<_$X*RoWo>@wro6rcp`xQZaXGPFcGZmfFyrY94UN) z;^N}0L(t_L^0?c!!;n{TtG=XJT*pp~%slyFDy4uGACUk_k#v6#!vl~v0&E!MZ=o{c zcY9%QUA%s2*GjrmaQmOvTL%3+?l^|5g&MMMn%)8{VUzqGMrNd)6Mr5kOzU4 zKpyA*yMP7|^A3|Sdi3xUE{;l>uRY>AM<_56uEXu{@C{6^ zgwFqsh!ny3V;^Yda*Xu*_3r||Anb|G&X2Ij(!Dxbki7i{|4D_Wpfu)JyLRJ74ImoO z@eV-9W}HeBiyyP((Z8A17xHBY002cS%a^CSDpCAk!I*DHM5GY@7SZR_hX=R#i1vYK zQwuF|>E5;-3|!~#=H=}ZG08Ci^iffiJ=-L?a{W&KV0vaa@MVIUP)ifgWmhn{pN86< zi*%}J%Z!Es@>vcCP=2s>XA{8eiwnPwlaLNg2zWsjS6Bwb;KC2X}e79sf{|JXlc!aNma2*)lZTzQZ86Ck1p{Z!YwI;8l zjM%sY^H_YfmQaM<+tU_?3%K;uLjc#$S`naX%hg2`yk;xHzQw0 z`cfCVKeaLJGC$@S`7kjvX8(QYw4#j+masstCZagrJ&1Qzfja?B&{;*pX)OmW2|`2( zoe#JMkvu%zML@1SG|S3INhr{_vks|la#ap6XSJtZ@^%-EXOZ82L!TvzEol}Sc%L*^ z_;bqNldtl%I}h+6U*_Q8z^{g@i@l)hVk0-SK~~4Mh@)BOxLJn~P}OK)v3bE*qy0jW zIr2};IuBAJj{t9?5c>v$9R4Sz+Ci0z`voo=z;UZkWTROl&mSh)~D6ca=EwEqndQT!JpOP&)zMD%nx`F{dLROCrEN68OA7OEe_OjU!+umYJ2V}=M+&qX5$f1<_S_Rc?hnb1<=!{*n)D`?Wfle9L78v^& z3*K!<$RqD&Sl>n_u~$Ko>q zKp=+!fVc^>({y$ZH=~cYVwG+eC|^m|p~h9KR)KLwZ}Qo8OG?L?%`tSnnfGi4@(T(F zel^l;)0~@09L}wq`+g?957g@t=gZ7oka5f3y$eNQ4Nkc1cbim8Cl&c%OLyvfq(9K= zAQMQY^qd~M-T344MHbo9*cibEp*geD<7n0(>@ckZiVu3b&?XU&c2Z(?U<1|o2Jq%c zq$p^Rzh=-tfe$NaMCw`?1I?k$B%4=@XMi6?)EGd>K#_$8Z(_Z+6%1So>0tf9qQYFH z;`O1U2f-JOraN*plFJoTuQsNi3c?gX7$imh2Y5sQ6-2%2Z^=Ibd8w!Ye1gI zi825>8Gkh}Zc`e_1-&KLW#A6qLG-hqQ*Mb=pa-b%0VZfq24o;j_gDwO8j{Q0tJT`4 zLA3dEQfKy8{d)L+z(^DX`hrV9FSx+C3Z&UbZQ06|sLUbFBAieY$oeW|5&%t1g8n&@ z!9Wg>T!$Jy10k;HA*&-XB2MF^vEh~u$iWx@nR#IVQHFfKVL+d(^HDG-3T3_p=z}av zm<)9LHX>hBE*B$>bO5sujl(RArO+7_gm>=z zM5YHIkTyVZ17Cm{0yoJB;c5cbfD(#AERkHH;Ia;M(2W~t7H*L*D=8_V=T2GRiWX6R zev#7t05#AL(N6CKkNqd)iGhRRBavb<19Y^Cy%++ZeGCK&e3HPYM))w$-9!UXWTrS)D2Sc<%@}b*dPQYMaPL1EBmB;;Ahr#^6G0hq z4T7@z0(XiU$r>_70(MR;)sMww&CDS4pwUr8Edj2zr2}2jIKmSlp&{j5^Y=h+%7&dMnYckmu_>_ZENo3d zgHyM*_f!Z7XkImRHddjL7BqZz(I{F677$ZR4Dkyohpro>9p-%dn;x{qsz$XlIx}3^ z2VwOSh-fry8<9>t(xS{q^eUuIqLg0a{5n6H;!SE^j?J4>mag8~jV^}Ud}x|dMq?iQ zAfJ5IQZ_Ii_LH8DVv-81x=4EYbFkpdO(p|hGBk*kL1_Gr5N%@m4bslf&=(Jaaeq(# zcuO_DP|0xku7&2!1)gc7RBlcesZJM|6;YRzIru4mvg`hr9H&GfqvU5W53q)5IbahY zQ6jK?B93i?lw%y}V8XDsyOgk!oq<)h;^*4ranCdf34xxPg|R_(=|N=u{k_#&Z7-g% z#KA^VpQz~_J{%5ZG#V4wKyE}YW(4W0#0M^ks4dU{Qy0vw#HWTD(+FxFI~wG|D#&*p za0d5+W0ZW=Ie$|<(I2$2`Rw}FwvKuZh6A+SN9PKcH_l#PsFg^iFgH;uXN|%E{MD!N zmiO>n35#-eQtg1oslf#XMP6a$NX4~DDvAkae9>5&sdiaTbm)|PGk=bK&y85`nZhs= z>1P?z+Jv-1)#io{CXNjo+)D3r;=?M$o>)-HY6OxJ%>xbc0_Z3*Ha3Q=dXJB&<)mde z`DpeNfXdTo^`J$go2Cb?s$_Y~mMtSl9yN2=hZBn^0SV5>VWF&H?Bjtio@sx)t`zN> zazHHR!2BQD35L`DUtDIc1Eq6HX3W`c)qeC0IZO~uoA{#OGZC-gY~-6P>%TwL5?f&A zOuZ&8;2{K{Lqpi^4m`whb3f{{nZ*_B9{6%^&}Ke5;7NZRMXu)xZ^;b*TM z^fJdUI@b}|2;~&`GSUa(2|fj?`_eD7bW}OBR(9A6t@#Lp9%iw*RfmuC4sKS+@AICm z^FDay%rkPph(@a%s5p6a?FBR!hB(i=f2_#K6UlIUd&h~QKA<0kTTb(j3Z}zn)9X6yJcCmB7WvRUrZ4iTqB|zv wP3_p?NUdX)qE;|*QA_-pDSAc5|MWAHzDHfu*_eBx7=uz%(Na!UJm&Ym0AVn9f&c&j diff --git a/doc/source/tutorials/quickstart/figures/social_network.png b/doc/source/tutorials/quickstart/figures/social_network.png new file mode 100644 index 0000000000000000000000000000000000000000..4c1b2d2fa9dafd78cb14b1922ee400724b65baa8 GIT binary patch literal 26448 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 literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 1b48e1b7d..c8a57c355 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -1,7 +1,16 @@ =========== Quick Start =========== -This is a short condensed segment aimed at those who want a very quick and dirty rundown on the basics of *igraph*. For a more in depth explanation about what each part of this code is doing, check out `tutorial link.<>`_ + +This quick start will demonstrate the following: + +- Construct a *igraph* graph from scratch +- Set the attributes of nodes and edges +- Plot out a graph using matplotlib +- Save a graph as an image +- Export and import a graph as a ``.gml`` file + +This example is aimed at those with some familiarity with python and/or graphing packages, who would like to get some code up and running as fast as possible. .. code-block:: python @@ -10,50 +19,50 @@ This is a short condensed segment aimed at those who want a very quick and dirty # Construct a graph with 3 vertices n_vertices = 3 - edges = [(0, 1), (1, 2), (2, 0)] + edges = [(0, 1), (0, 2), (0, 3), (0, 4), (1, 2), (1, 3), (1, 4), (3, 4)] g = ig.Graph(n_vertices, edges) # Set attributes for the graph, nodes, and edges - g["title"] = "Cities of America" - g.vs["name"] = ["New York City", "San Francisco", "Los Angeles"] - g.vs["population"] = [8.62e+6, 8.74e+5, 4.08e+6] - g.es["distance"] = [4160, 559, 3980] + g["title"] = "Small Social Network" + g.vs["name"] = ["Daniel Morillas", "Kathy Archer", "Kyle Ding", "Joshua Walton", "Jana Hoyer"] + g.vs["gender"] = ["M", "F", "F", "M", "F"] + g.es["married"] = [False, False, False, False, False, False, False, True] # Set individual attributes - g.vs[2]["name"] = "Chicago" - g.vs[2]["population"] = 2.71e+6 - g.es[1]["distance"] = 2960 - g.es[2]["distance"] = 1180 + g.vs[1]["name"] = "Kathy Morillas" + g.es[0]["married"] = True # Plot in matplotlib + # Note that attributes can be set globally (e.g. vertex_size), or set individually using arrays (e.g. vertex_color) fig, ax = plt.subplots(figsize=(5,5)) ig.plot( g, target=ax, - layout=g.layout("circle"), # print nodes in a circular layout - vertex_size=0.07, # set vertex_size of all vertices at once - vertex_color=["lightblue", "orange", "orange"], # set colours individually - vertex_frame_color=["white", "white", "white"], + layout="circle", # print nodes in a circular layout + vertex_size=0.1, + vertex_color=["lightblue" if gender == "M" else "pink" for gender in g.vs["gender"]], vertex_frame_width=2.0, - vertex_shape=["circle", "rectangle", "triangle-down"], + vertex_frame_color="white", vertex_label=g.vs["name"], vertex_label_size=7.0, - edge_width=[1.2+dist/4000.0 for dist in g.es["distance"]], # longer distances = thicker edges + edge_width=[2 if married else 1 for married in g.es["married"]], ) plt.show() # Save the graph as an image file - fig.savefig('america.png') - fig.savefig('america.jpg') - fig.savefig('america.pdf') + fig.savefig('social_network.png') + fig.savefig('social_network.jpg') + fig.savefig('social_network.pdf') # Export and import a graph as a GML file. - g.save("america.gml") - g = ig.load("america.gml") + g.save("social_network.gml") + g = ig.load("social_network.gml") + -... and here is the graph generated by the code: +The output of the code is pictured below -.. figure:: ./figures/america.png - :alt: The visual representation of our city network - :align: center \ No newline at end of file +.. figure:: ./figures/social_network.png + :alt: The visual representation of a small friendship group + :align: center + The Output Graph From c83e057f6fa9c992e09a3edf2df124d1b43155dd Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Tue, 23 Nov 2021 19:14:39 +1100 Subject: [PATCH 0523/1681] Fix some minor mistakes --- .../assets/bipartite_mathing.py | 1 + .../bipartite_matching/bipartite_matching.rst | 13 +++---- .../shortest_paths/assets/shortest_path.py | 36 +++++++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 doc/source/tutorials/shortest_paths/assets/shortest_path.py diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py index 7a1f95fcb..790957270 100644 --- a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py @@ -36,6 +36,7 @@ ) plt.show() +# Output: # Matching is: # 0 - 5 # 1 - 7 diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index c8a2339f0..46703524d 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -40,20 +40,15 @@ And finally display the bipartite graph with matchings highlighted. .. code-block:: python - # Manually set the position of source and sink to display nicely - layout = g.layout_bipartite() - layout[9] = (2, -1) - layout[10] = (2, 2) - - fig, ax = plt.subplots() + fig, ax = plt.subplots(figsize=(7, 3)) ig.plot( g, target=ax, - layout=layout, + layout=g.layout_bipartite(), vertex_size=0.4, vertex_label=range(g.vcount()), - vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], - edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] + vertex_color="lightblue", + edge_width=[2.5 if e.target==matching.match_of(e.source) else 1.0 for e in g.es] ) plt.show() diff --git a/doc/source/tutorials/shortest_paths/assets/shortest_path.py b/doc/source/tutorials/shortest_paths/assets/shortest_path.py new file mode 100644 index 000000000..1234c1e14 --- /dev/null +++ b/doc/source/tutorials/shortest_paths/assets/shortest_path.py @@ -0,0 +1,36 @@ +import igraph as ig +import matplotlib.pyplot as plt + +# Find the shortest path on an unweighted graph +g = ig.Graph( + 6, + [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] +) + +# g.get_shortest_paths() returns a list of vertex ID paths +results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] + +if len(results[0]) > 0: + # The distance is the number of vertices in the shortest path minus one. + print("Shortest distance is: ", len(results[0])-1) +else: + print("End node could not be reached!") + +# Find the shortest path on a weighted graph +g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] + +# g.get_shortest_paths() returns a list of edge ID paths +results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] + +if len(results[0]) > 0: + # Add up the weights across all edges on the shortest path + distance = 0 + for e in results[0]: + distance += g.es[e]["weight"] + print("Shortest weighted distance is: ", distance) +else: + print("End node could not be reached!") + +# Output: +# Shortest distance is: 3 +# Shortest weighted distance is: 8 \ No newline at end of file From 3d074b2f3433abbfbbf4e7748c328a1c91da39a7 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 24 Nov 2021 20:32:25 +0100 Subject: [PATCH 0524/1681] fix: fix drawing of graphs with a single graph and a single loop edge when using the Maptlotlib backend, fixes #462 --- src/igraph/drawing/graph.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index 41a950110..5210eb256 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -993,7 +993,7 @@ def shrink_vertex(ax, aux, vcoord, vsize_squared): """Shrink edge by vertex size""" aux_display, vcoord_display = ax.transData.transform([aux, vcoord]) d = sqrt(((aux_display - vcoord_display) ** 2).sum()) - fr = sqrt(vsize_squared) / d + fr = sqrt(vsize_squared) / d if d > 0 else 0 end_display = vcoord_display + fr * (aux_display - vcoord_display) end = ax.transData.inverted().transform(end_display) return end @@ -1178,10 +1178,19 @@ def callback_edge_offset(event): xi, yi = x[i], y[i] ax.text(xi, yi, lab, fontsize=label_size) - dx = max(x) - min(x) - dy = max(y) - min(y) - ax.set_xlim(min(x) - 0.05 * dx, max(x) + 0.05 * dx) - ax.set_ylim(min(y) - 0.05 * dy, max(y) + 0.05 * dy) + # Find the X and Y range of coordinates; use a minimum range even if there + # is only one vertex to avoid singularities and division by zero later + dx, dy = max(x) - min(x), max(y) - min(y) + if dx <= 0: + dx = 1 + ax.set_xlim(min(x) - dx / 2, max(x) + dx / 2) + else: + ax.set_xlim(min(x) - 0.05 * dx, max(x) + 0.05 * dx) + if dy <= 0: + dy = 1 + ax.set_ylim(min(y) - dy / 2, max(y) + dy / 2) + else: + ax.set_ylim(min(y) - 0.05 * dy, max(y) + 0.05 * dy) # Edge properties ne = graph.ecount() From 8e715812d3f5efaffa1eee5f0582756498e5f180 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Fri, 26 Nov 2021 00:57:36 +1100 Subject: [PATCH 0525/1681] Unarchive bipartite matching using max flow --- .../bipartite_matching/bipartite_matching.rst | 60 ------------- .../assets/bipartite_matching_maxflow.py} | 13 ++- .../bipartite_matching_maxflow.rst | 84 ++++++++++++++++++ .../figures/bipartite_matching_maxflow.png} | Bin doc/source/tutorials/maxflow/maxflow.rst | 4 - 5 files changed, 96 insertions(+), 65 deletions(-) delete mode 100644 doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst rename doc/source/tutorials/{archived/bipartite_matching/assets/maxflow2.py => bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py} (69%) create mode 100644 doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst rename doc/source/tutorials/{archived/bipartite_matching/figures/maxflow2.png => bipartite_matching_maxflow/figures/bipartite_matching_maxflow.png} (100%) diff --git a/doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst deleted file mode 100644 index 4dcef0291..000000000 --- a/doc/source/tutorials/archived/bipartite_matching/bipartite_matching.rst +++ /dev/null @@ -1,60 +0,0 @@ -========================== -Maximum Bipartite Matching -========================== - -This example demonstrates how to visualise bipartite matching using max flow. - -.. code-block:: python - - # Generate the graph - g = ig.Graph( - 9, - [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], - directed=True - ) - - # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side - for i in range(4): - g.vs[i]["type"] = True - for i in range(4, 9): - g.vs[i]["type"] = False - - g.add_vertices(2) - g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side - g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other - - flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1 - print("Size of the Maximum Matching is:", flow.value) - -And to display the flow graph nicely, with the matchings added - -.. code-block:: python - - # Manually set the position of source and sink to display nicely - layout = g.layout_bipartite() - layout[9] = (2, -1) - layout[10] = (2, 2) - - fig, ax = plt.subplots() - ig.plot( - g, - target=ax, - layout=layout, - vertex_size=0.4, - vertex_label=range(g.vcount()), - vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], - edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] - ) - plt.show() - -The received output is: - -.. code-block:: - - Maximal Matching is: 4.0 - -.. figure:: ./figures/maxflow2.png - :alt: The visual representation of maximal bipartite matching - :align: center - - Maximal Bipartite Matching \ No newline at end of file diff --git a/doc/source/tutorials/archived/bipartite_matching/assets/maxflow2.py b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py similarity index 69% rename from doc/source/tutorials/archived/bipartite_matching/assets/maxflow2.py rename to doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py index 844d12306..5fa0c3e82 100644 --- a/doc/source/tutorials/archived/bipartite_matching/assets/maxflow2.py +++ b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py @@ -19,7 +19,18 @@ g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other flow = g.maxflow(9, 10) -print("Size of the Maximal Matching is:", flow.value) +print("Size of Maximum Matching (maxflow) is:", flow.value) + +# Compare this to the "maximum_bipartite_matching()" function +g2 = g.copy() +g2.delete_vertices([9, 10]) # delete the source and sink, which are unneeded for this function. +matching = g2.maximum_bipartite_matching() + +matching_size = 0 +for i in range(4): + if matching.match_of(i): + matching_size += 1 +print("Size of Maximum Matching (maximum_bipartite_matching) is:", matching_size) # Manually set the position of source and sink to display nicely layout = g.layout_bipartite() diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst new file mode 100644 index 000000000..2f7145a05 --- /dev/null +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -0,0 +1,84 @@ +========================================== +Maximum Bipartite Matching by Maximum Flow +========================================== + +This example presents how to visualise bipartite matching using maximum flow. Please note that the *igraph* already has :meth:`maximum_bipartite_matching` which is better suited for finding the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching<>`_. This particular example is purely for demonstrative purposes. + +.. TODO: add link to Maximum Bipartite Matching + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + + # Generate the graph + g = ig.Graph( + 9, + [(0, 4), (0, 5), (1, 4), (1, 6), (1, 7), (2, 5), (2, 7), (2, 8), (3, 6), (3, 7)], + directed=True + ) + + # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side + for i in range(4): + g.vs[i]["type"] = True + for i in range(4, 9): + g.vs[i]["type"] = False + + g.add_vertices(2) + g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side + g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other + + flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1.0 + print("Size of Maximum Matching (maxflow) is:", flow.value) + +Let's compare the output against :meth:`maximum_bipartite_matching` + +.. code-block:: python + + # Compare this to the "maximum_bipartite_matching()" function + g2 = g.copy() + g2.delete_vertices([9, 10]) # delete the source and sink, which are unneeded + + matching = g2.maximum_bipartite_matching() + + matching_size = 0 + for i in range(4): + if matching.match_of(i): + matching_size += 1 + print("Size of Maximum Matching (maximum_bipartite_matching) is:", matching_size) + +And finally, display the flow graph nicely with the matchings added + +.. code-block:: python + + # Manually set the position of source and sink to display nicely + layout = g.layout_bipartite() + layout[9] = (2, -1) + layout[10] = (2, 2) + + fig, ax = plt.subplots() + ig.plot( + g, + target=ax, + layout=layout, + vertex_size=0.4, + vertex_label=range(g.vcount()), + vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], + edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] + ) + plt.show() + +The received output is: + +.. code-block:: + + Size of Maximum Matching (maxflow) is: 4.0 + Size of Maximum Matching (maximum_bipartite_matching) is: 4 + +.. figure:: ./figures/bipartite_matching_maxflow.png + :alt: The visual representation of maximal bipartite matching + :align: center + + Maximal Bipartite Matching + +Note that maximum flow will represent the capacities as real values, which is why our result is ``4.0`` instead of ``4``. diff --git a/doc/source/tutorials/archived/bipartite_matching/figures/maxflow2.png b/doc/source/tutorials/bipartite_matching_maxflow/figures/bipartite_matching_maxflow.png similarity index 100% rename from doc/source/tutorials/archived/bipartite_matching/figures/maxflow2.png rename to doc/source/tutorials/bipartite_matching_maxflow/figures/bipartite_matching_maxflow.png diff --git a/doc/source/tutorials/maxflow/maxflow.rst b/doc/source/tutorials/maxflow/maxflow.rst index 152aec214..84cd755fa 100644 --- a/doc/source/tutorials/maxflow/maxflow.rst +++ b/doc/source/tutorials/maxflow/maxflow.rst @@ -9,10 +9,6 @@ This example shows how to construct a max flow on a directed graph with edge cap import igraph as ig import matplotlib.pyplot as plt -Max flow on a directed weighted flow graph - -.. code-block:: python - # Generate the graph with its capacities g = ig.Graph( 6, From a204c7670edd7569b011e5ca14c66d394c191795 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Fri, 26 Nov 2021 00:57:51 +1100 Subject: [PATCH 0526/1681] Update bipartite_matching to use Graph.Bipartite --- .../assets/bipartite_mathing.py | 18 ++++++----------- .../bipartite_matching/bipartite_matching.rst | 20 +++++++++---------- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py index 790957270..65c5d83dc 100644 --- a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py @@ -1,17 +1,12 @@ import igraph as ig import matplotlib.pyplot as plt -g = ig.Graph( - 9, +# Assign nodes 0-4 to one side, and the nodes 5-8 to the other side +g = ig.Graph.Bipartite( + [0,0,0,0,0,1,1,1,1], [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) - -# Assign nodes 0-4 to one side, and the nodes 5-8 to the other side -for i in range(5): - g.vs[i]["type"] = True -for i in range(5, 9): - g.vs[i]["type"] = False - +assert(g.is_bipartite()) matching = g.maximum_bipartite_matching() @@ -36,11 +31,10 @@ ) plt.show() -# Output: # Matching is: # 0 - 5 # 1 - 7 # 2 - 8 -# 3 - None -# 4 - 6 +# 3 - 6 +# 4 - None # Size of Maximum Matching is: 4 diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index 46703524d..59c636a52 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -2,23 +2,21 @@ Maximum Bipartite Matching ========================== -This example demonstrates how to find and visualise a maximum biparite matching. First construct a bipartite graph +This example demonstrates an efficient way to find and visualise a maximum biparite matching. First construct a bipartite graph .. code-block:: python import igraph as ig import matplotlib.pyplot as plt - g = ig.Graph( - 9, + # Assign nodes 0-4 to one side, and the nodes 5-8 to the other side + g = ig.Graph.Bipartite( + [0,0,0,0,0,1,1,1,1], [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) + assert(g.is_bipartite()) - # Assign nodes 0-4 to one side, and the nodes 5-8 to the other side - for i in range(5): - g.vs[i]["type"] = True - for i in range(5, 9): - g.vs[i]["type"] = False + matching = g.maximum_bipartite_matching() Then run the maximum matching, @@ -60,12 +58,12 @@ The received output is 0 - 5 1 - 7 2 - 8 - 3 - None - 4 - 6 + 3 - 6 + 4 - None Size of Maximum Matching is: 4 .. figure:: ./figures/bipartite.png :alt: The visual representation of maximal bipartite matching :align: center - Maximum Bipartite Matching \ No newline at end of file + Maximum Bipartite Matching From 2f94b19c3a9410d6a9ef569c0d48d9d0537bd756 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Fri, 26 Nov 2021 01:19:50 +1100 Subject: [PATCH 0527/1681] Add Erdos Renyi Graph generator example --- .../bipartite_matching_maxflow.rst | 4 +- .../erdos_renyi/assets/erdos_renyi.py | 57 +++++++++++ .../tutorials/erdos_renyi/erdos_renyi.rst | 91 ++++++++++++++++++ .../erdos_renyi/figures/erdos_renyi_m.png | Bin 0 -> 153627 bytes .../erdos_renyi/figures/erdos_renyi_p.png | Bin 0 -> 100941 bytes 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py create mode 100644 doc/source/tutorials/erdos_renyi/erdos_renyi.rst create mode 100644 doc/source/tutorials/erdos_renyi/figures/erdos_renyi_m.png create mode 100644 doc/source/tutorials/erdos_renyi/figures/erdos_renyi_p.png diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index 2f7145a05..97b36c08d 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -4,7 +4,7 @@ Maximum Bipartite Matching by Maximum Flow This example presents how to visualise bipartite matching using maximum flow. Please note that the *igraph* already has :meth:`maximum_bipartite_matching` which is better suited for finding the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching<>`_. This particular example is purely for demonstrative purposes. -.. TODO: add link to Maximum Bipartite Matching +.. TODO: add link to Maximum Bipartite Matching .. code-block:: python @@ -47,7 +47,7 @@ Let's compare the output against :meth:`maximum_bipartite_matching` matching_size += 1 print("Size of Maximum Matching (maximum_bipartite_matching) is:", matching_size) -And finally, display the flow graph nicely with the matchings added +And finally, display the original flow graph nicely with the matchings added .. code-block:: python diff --git a/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py b/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py new file mode 100644 index 000000000..1b8127965 --- /dev/null +++ b/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py @@ -0,0 +1,57 @@ +import igraph as ig +import matplotlib.pyplot as plt +import random + +# Set a random seed for reproducibility +random.seed(0) + +# Generate two Erdos Renyi graphs based on probability +g1 = ig.Graph.Erdos_Renyi(n=15, p=0.2, directed=False, loops=False) +g2 = ig.Graph.Erdos_Renyi(n=15, p=0.2, directed=False, loops=False) + +# Generate two Erdos Renyi graphs based on number of edges +g3 = ig.Graph.Erdos_Renyi(n=20, m=35, directed=False, loops=False) +g4 = ig.Graph.Erdos_Renyi(n=20, m=35, directed=False, loops=False) + +# Print out summaries of each graph +ig.summary(g1) +ig.summary(g2) +ig.summary(g3) +ig.summary(g4) + +fig, axs = plt.subplots(1, 2, figsize=(10, 5)) +ig.plot( + g1, + target=axs[0], + layout="circle", + vertex_color="lightblue" +) +ig.plot( + g2, + target=axs[1], + layout="circle", + vertex_color="lightblue" +) +plt.show() + +fig, axs = plt.subplots(1, 2, figsize=(10, 5)) +ig.plot( + g3, + target=axs[0], + layout="circle", + vertex_color="lightblue", + vertex_size=0.15 +) +ig.plot( + g4, + target=axs[1], + layout="circle", + vertex_color="lightblue", + vertex_size=0.15 +) +plt.show() + +# IGRAPH U--- 15 18 -- +# IGRAPH U--- 15 21 -- +# IGRAPH U--- 20 35 -- +# IGRAPH U--- 20 35 -- diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst new file mode 100644 index 000000000..cee25705e --- /dev/null +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -0,0 +1,91 @@ +================= +Erdős-Rényi Graph +================= + +This example demonstrates how to generate `Erdős-Rényi Graphs`_. There are two variants of graphs: + +- :meth:`Erdos_Renyi(n, m)` will pick a graph uniformly at random out of all graphs with ``n`` nodes and ``m`` edges. +- :meth:`Erdos_Renyi(n, p)` will generate a graph where each edge between any two pair of nodes has an independent probability ``p`` of existing. + +We generate two graphs of each, so we can confirm that our graph generator is truly random. + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + import random + + # Set a random seed for reproducibility + random.seed(0) + + # Generate two Erdos Renyi graphs based on probability + g1 = ig.Graph.Erdos_Renyi(n=15, p=0.2, directed=False, loops=False) + g2 = ig.Graph.Erdos_Renyi(n=15, p=0.2, directed=False, loops=False) + + # Generate two Erdos Renyi graphs based on number of edges + g3 = ig.Graph.Erdos_Renyi(n=20, m=35, directed=False, loops=False) + g4 = ig.Graph.Erdos_Renyi(n=20, m=35, directed=False, loops=False) + + # Print out summaries of each graph + ig.summary(g1) + ig.summary(g2) + ig.summary(g3) + ig.summary(g4) + + fig, axs = plt.subplots(1, 2, figsize=(10, 5)) + ig.plot( + g1, + target=axs[0], + layout="circle", + vertex_color="lightblue" + ) + ig.plot( + g2, + target=axs[1], + layout="circle", + vertex_color="lightblue" + ) + plt.show() + + fig, axs = plt.subplots(1, 2, figsize=(10, 5)) + ig.plot( + g3, + target=axs[0], + layout="circle", + vertex_color="lightblue", + vertex_size=0.15 + ) + ig.plot( + g4, + target=axs[1], + layout="circle", + vertex_color="lightblue", + vertex_size=0.15 + ) + plt.show() + +The received output is: + +.. code-block:: + + IGRAPH U--- 15 18 -- + IGRAPH U--- 15 21 -- + IGRAPH U--- 20 35 -- + IGRAPH U--- 20 35 -- + +.. figure:: ./figures/erdos_renyi_p.png + :alt: The visual representation of a randomly generated Erdos Renyi graph + :align: center + + Erdos Renyi random graphs with probability ``p`` = 0.2 + +.. figure:: ./figures/erdos_renyi_m.png + :alt: The second visual representation of a randomly generated Erdos Renyi graph + :align: center + + Erdos Renyi random graphs with ``m`` = 35 edges + +Note that even when using the same random seed, results can still differ depending on the machine the code is being run from. + + + diff --git a/doc/source/tutorials/erdos_renyi/figures/erdos_renyi_m.png b/doc/source/tutorials/erdos_renyi/figures/erdos_renyi_m.png new file mode 100644 index 0000000000000000000000000000000000000000..455c3bd28862d68fcd61e27084bb9309b79400ba GIT binary patch literal 153627 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
    +jKwqQ*f&j!qIg{)8^XJ3$E>@FM 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 literal 0 HcmV?d00001 From 0950bb4903c43120312c7118db2eb22370a902e4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Nov 2021 10:04:36 +0100 Subject: [PATCH 0528/1681] fix: setup.py develop works again, refs #464 --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 86b13d57b..d4d5202a9 100644 --- a/setup.py +++ b/setup.py @@ -823,7 +823,12 @@ def use_educated_guess(self) -> None: "Source Code": "https://github.com/igraph/python-igraph", }, ext_modules=[igraph_extension], - package_dir={"igraph": "src/igraph"}, + package_dir={ + # make sure to use the next line and not the more logical and restrictive + # "igraph": "src/igraph" because that one breaks 'setup.py develop'. + # See: https://github.com/igraph/python-igraph/issues/464 + "": "src" + }, packages=["igraph", "igraph.app", "igraph.drawing", "igraph.remote"], scripts=["scripts/igraph"], install_requires=["texttable>=1.6.2"], From 9704fb59a5a84507ce845298d0b25f9200f1aa5d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 26 Nov 2021 10:06:37 +0100 Subject: [PATCH 0529/1681] fix: add Homebrew lib dir to the list of folders where we look for igraph libs --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d4d5202a9..8e9d29b76 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def find_static_library(library_name: str, library_path: List[str]) -> Optional[ variants = ["lib{0}.a", "{0}.a", "{0}.lib", "lib{0}.lib"] if is_unix_like(): extra_libdirs = [ + "/opt/homebrew/lib", # for newer Homebrew installations on macOS "/usr/local/lib64", "/usr/local/lib", "/usr/lib/x86_64-linux-gnu", From 25c0f9bbbf462a591c41140f292b71c1f4c9e3b0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 29 Nov 2021 09:16:22 +0100 Subject: [PATCH 0530/1681] fix: tutorial incorrectly indicated that colors as RGB tuples/lists use the range 0-255, fixes #465 --- doc/source/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 86a29a4a4..df8a0cf2a 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -959,8 +959,8 @@ Color specification in CSS syntax - ``rgb(R, G, B)``, components range from 0 to 255 or from 0% to 100%. Example: ``"rgb(0, 127, 255)"`` or ``"rgb(0%, 50%, 100%)"``. -List, tuple or whitespace-separated string of RGB values - Example: ``(255, 128, 0)``, ``[255, 128, 0]`` or ``"255, 128, 0"``. +Lists or tuples of RGB values in the range 0-1 + Example: ``(1.0, 0.5, 0)`` or ``[1.0, 0.5, 0]``. Saving plots From c2a48432fc3aa6123052673678e2375cba36b2aa Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 30 Nov 2021 07:15:19 +1100 Subject: [PATCH 0531/1681] fix: center vertex labels in matplotlib --- src/igraph/drawing/matplotlib/graph.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py index 4578249e2..3c7742efc 100644 --- a/src/igraph/drawing/matplotlib/graph.py +++ b/src/igraph/drawing/matplotlib/graph.py @@ -255,7 +255,9 @@ def draw(self, graph, *args, **kwds): *coords, vertex.label, fontsize=label_size, - # TODO: alignment, overlap, offset, etc. + ha='center', + va='center', + # TODO: overlap, offset, etc. ) # Construct the iterator that we will use to draw the edges From faeb9bf3e6c0580c7164a0e6deb5e3be7f14f336 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 30 Nov 2021 15:23:41 +1100 Subject: [PATCH 0532/1681] Custom edge colors --- .../tutorials/quickstart/assets/quickstart.py | 1 + .../quickstart/figures/social_network.png | Bin 26448 -> 26637 bytes .../tutorials/quickstart/quickstart.rst | 1 + 3 files changed, 2 insertions(+) diff --git a/doc/source/tutorials/quickstart/assets/quickstart.py b/doc/source/tutorials/quickstart/assets/quickstart.py index 2db46d73b..2114377c9 100644 --- a/doc/source/tutorials/quickstart/assets/quickstart.py +++ b/doc/source/tutorials/quickstart/assets/quickstart.py @@ -30,6 +30,7 @@ vertex_label=g.vs["name"], vertex_label_size=7.0, edge_width=[2 if married else 1 for married in g.es["married"]], + edge_color=["#F00" if married else "#000" for married in g.es["married"]], ) plt.show() diff --git a/doc/source/tutorials/quickstart/figures/social_network.png b/doc/source/tutorials/quickstart/figures/social_network.png index 4c1b2d2fa9dafd78cb14b1922ee400724b65baa8..562ca0541aaaaad10b70de9702f69209113b5f3c 100644 GIT binary patch literal 26637 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 z'yf9Qc*%Di>9ziNF=+1$dyxaiEm!lo={1`cNRf5sk!9*ooGkt&DwXAdACTDi! zOLus_c`4h@JJkEBxyI3}B4A9X>Irsj11dXFM@{o`F8y!f*4Mk7Qtg#+l~f z;9@WMCPZh{+pvdd)4-V#ekJ^R`ucT&WE9qpj@N+S9F8P9WcOBto_v&NMwXRD)6dV( zr0Wsj)2_(iuIdiz{C=+g&hqs3!}m3$5;9M}ji0EStHw=?NlV*;l9fp}B@wS2#Z6(~ zOxnA*dUd(Fz@|EgBCumpLq{nxD32Q7Iyot+D||e6b$P*VtUcQdXa{Ho3+8!i8=2Q- zvZF_B^C_?KtVMXr zoqXxmZ2B}4TAdL!HI1fd+e7=jqjg5v*toe9hL)yFN)!mXwDXR^hwyuei4m%zk=tZt zS^bt~gF-_oKjmBHKex%7Q}Q4meory{j%?@7kmU#A;S{O9C>~i&JfljuhTY&;ICW

  • v1rtwQuh(6Rat3QXuUpL9ee_v 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 I?D>cO2NEF>V*mgE diff --git a/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py b/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py index 1b8127965..b546d0dfd 100644 --- a/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py +++ b/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py @@ -26,12 +26,14 @@ layout="circle", vertex_color="lightblue" ) +axs[0].set_aspect(1) ig.plot( g2, target=axs[1], layout="circle", vertex_color="lightblue" ) +axs[1].set_aspect(1) plt.show() fig, axs = plt.subplots(1, 2, figsize=(10, 5)) @@ -42,6 +44,7 @@ vertex_color="lightblue", vertex_size=0.15 ) +axs[0].set_aspect(1) ig.plot( g4, target=axs[1], @@ -49,6 +52,7 @@ vertex_color="lightblue", vertex_size=0.15 ) +axs[1].set_aspect(1) plt.show() # IGRAPH U--- 15 18 -- diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index cee25705e..41da76d88 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -39,12 +39,14 @@ We generate two graphs of each, so we can confirm that our graph generator is tr layout="circle", vertex_color="lightblue" ) + axs[0].set_aspect(1) ig.plot( g2, target=axs[1], layout="circle", vertex_color="lightblue" ) + axs[1].set_aspect(1) plt.show() fig, axs = plt.subplots(1, 2, figsize=(10, 5)) @@ -55,6 +57,7 @@ We generate two graphs of each, so we can confirm that our graph generator is tr vertex_color="lightblue", vertex_size=0.15 ) + axs[0].set_aspect(1) ig.plot( g4, target=axs[1], @@ -62,6 +65,7 @@ We generate two graphs of each, so we can confirm that our graph generator is tr vertex_color="lightblue", vertex_size=0.15 ) + axs[1].set_aspect(1) plt.show() The received output is: diff --git a/doc/source/tutorials/erdos_renyi/figures/erdos_renyi_m.png b/doc/source/tutorials/erdos_renyi/figures/erdos_renyi_m.png index 455c3bd28862d68fcd61e27084bb9309b79400ba..0d644d2116358fc9b83c4ae9b21dce84efaade3e 100644 GIT binary patch literal 135643 zcmeFZg;!N;)Hk{T>6Da^5D5tdrKCYDkWQ6GLK^7~326|e8%aqKLApUfLg@|(>F&65 zo$r0`xPQYP-x4g`;wSDs39)%({Kz^a+N#wppp`lPuWu(+xlGi33wbWi7VsC9K z`LW}DGsJ!ujOXO6hp&9|nXEE-_8OzQ8Ucy2G|5%VlF6bxt-N8GQcRgo20fdR$7Ont zNaAOG=Id5>MK^bK^-X%QsC%Bfm(<1j%nK3ulStz*A-^3L{q;J$@_+u0d;v}Pzc0gY zH^|r}+5Y$YUZF6T|M&0j2+~n0|NHaNXyoZ+|MOZ*cYV+u{`c=PSV}$$|NFX~D7-%0 z|9xR<$^XxrVEo@V`EOnS?{fXmj{3j5>;La{LjGUF`6TrG`E!@O<*QS5?${YhID~|R zUoD1-%+1Xk+|8jRF0$&qFC$J5H<@{PufBQnrf*de_-Z*Bf+?lvUYYhz33rc-ypwnU|^TFr=|o&bFnu9@bH5(>OInUtC;F zf^%ho&q%s6EeyK$yJs^(MCGQv?L$Q`4Qg$9AIVolnF*fFhI48OI#VVJT7>K__4FSj z?L9j-IXTSh{B-^}G)}9upV+F};m)>wfHV`)=8jb9lDXE)mo4MtNq!E}Ps)()-g(8_ z+q*lS7h{oAl*eq2Jip!L{O{G+gs_e+g9l}TTwzPpq%ep|c;khxdNWA)Qb=7joA^?p zW|TZuLtEcP*)%=P@Fz!p-Zc;|j}{pu4LaJ|bT2Bb!xheh^uMhJs)RcpcY0ZB3DI7? zdi9#1HJt?6O@ES|G_tE#eR0WH^{2+Tm+u=s%WvOtiVqT5QkdD!dYq@%Ag*}trvDi& zY@|+aC2Q;3LJ@EM{4Dw2lz#j6XnD%kQrL^Sz+7FHpR&Wwwt}zi(Aj2tZB#Yk3EPK? zG(jf+&P><|oj-p3kSSAy4b07LrItTT5+UxJT_5|&OEK_QUnKc!oZ2nt&6x_LMVv~5iX_s&9mwL8&9{garc~k%VWdGWIbMlpe z{4TGlj~tqhBI(5pOihD=h#4+ozh9MpSLkxI#k97z)|d5IQLR9mIwT|{DlYDEim)T^ zn*nwgS63z`rrFBw^@;EHF57eWFA`t>>XI%gctJtH6YtlU&iLumr!9CdG0zinA|j&Y z$z0-r0yW>v%p2t7FV+?|Yn{P?yk4$aJr-K2dnq*yxKfyS`4y&0X{+Y5wB;l-GJf$Wp|f zjGzDb=_$Y#`=Eo?PNuh|R^tM??Oefs|Gp9u64D+W>!_+W-etO7j8bNqlul~yZ3V(XmG=;pw zn52A`uz$0*hZ}8ZT;wM6;sIf2*AD|&QCoX^&f&SV*w|R1H*@3y^bOrFS%;R6gtUm3 z!#X>kgk2YjjpLP@`}Zfew5+TN{!o6HMFXEG^XX@Syccz>LJIGCx9wc^kI?8RYq1pK zLvN|(H2(QRHdsKRlb=eWBEUZVKFT|vj7_0&pqtbrU)w#+x``_c?aDIbGZ z*&+?5WB1Lb>%}G}CiIPrRB!*ARaREc(Fe)x35ebw9j)>_xOrfuLo6mH#-&$J7ZDM0H#$NQhsii#UZ|?N+HmxX zao+GJ>NNq&Al=I*B_%bkz3m^kdUhsy?C58;c7Kn(7bKq~&;&hZaAlxHyZ6_!u!|7+M-K9nqYpMug&Mr?-0%>b z9hvrK=3zuV2qd_*>q;u!svIK1bSdqAxY73RDsy`NAfE2y9-{%iP(7?ARC=BoTHT|F zwlR!c9{o(-#a~I-?(Xg%9{b5Zdq%KQ$zDDDosY)6qIJF_(uJXt=W!%`Te{TSblpm{ zAqMeCJ4%6v2c7ClYBIx2%ro?MbXhqsn?^^e;a)E6?6UetP5aIZX=!OS!q>7)wOYjH zu2d6IZ+Femp$gPKN5GG3c^`=u4CO~ky~UaxDKlEQI~^b2VV2i+S&=Ul8tZa)#Jf3v z5jHP0GCpoJRqJ}q%*?L>TQO_6v#f!j2QB@Lx?@{P?la+0nMy^k;^}l0)Cj_bS0@pJB~nmPWHQ≈T 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
    +jKwqQ*f&j!qIg{)8^XJ3$E>@FM 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

    wAN<}J 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 literal 100941 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 diff --git a/doc/source/tutorials/maxflow/assets/maxflow.py b/doc/source/tutorials/maxflow/assets/maxflow.py index b28b81bfc..643e135b7 100644 --- a/doc/source/tutorials/maxflow/assets/maxflow.py +++ b/doc/source/tutorials/maxflow/assets/maxflow.py @@ -22,6 +22,7 @@ vertex_label=range(g.vcount()), vertex_color="lightblue" ) +ax.set_aspect(1) plt.show() # Output: diff --git a/doc/source/tutorials/maxflow/figures/maxflow.png b/doc/source/tutorials/maxflow/figures/maxflow.png index 8cd123fe71d00501773db7b412696f4f462893e1..b370748d1c9b93745e094beb37ab4234ee90412b 100644 GIT binary patch literal 27330 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 literal 27217 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 literal 26637 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 z'yf9Qc*%Di>9ziNF=+1$dyxaiEm!lo={1`cNRf5sk!9*ooGkt&DwXAdACTDi! zOLus_c`4h@JJkEBxyI3}B4A9X>Irsj11dXFM@{o`F8y!f*4Mk7Qtg#+l~f z;9@WMCPZh{+pvdd)4-V#ekJ^R`ucT&WE9qpj@N+S9F8P9WcOBto_v&NMwXRD)6dV( zr0Wsj)2_(iuIdiz{C=+g&hqs3!}m3$5;9M}ji0EStHw=?NlV*;l9fp}B@wS2#Z6(~ zOxnA*dUd(Fz@|EgBCumpLq{nxD32Q7Iyot+D||e6b$P*VtUcQdXa{Ho3+8!i8=2Q- zvZF_B^C_?KtVMXr zoqXxmZ2B}4TAdL!HI1fd+e7=jqjg5v*toe9hL)yFN)!mXwDXR^hwyuei4m%zk=tZt zS^bt~gF-_oKjmBHKex%7Q}Q4meory{j%?@7kmU#A;S{O9C>~i&JfljuhTY&;ICW

    _+GO2wf&}`q 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

    9a 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 Q&kuFywz6FAEyEZ817w~z6951J literal 36485 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 diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_basic.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_basic.png index 551d858ccd9fdded438742b1690c0039977a0af4..c9883a651e68b79db0fc3509035925de187dd523 100644 GIT binary patch literal 24565 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 diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_edit_children.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_edit_children.png index 6763b66929d395f778bb8fb7bc36772b8869dd3a..33fa772b2dfe4aa8da5819da407060178cb44a0f 100644 GIT binary patch literal 24177 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 literal 24552 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 diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_directed.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_directed.png index 41279222155db204ff509001afff978525b6a811..fafa8fe0bb65f752e3c7b29d231208a8caddff4a 100644 GIT binary patch literal 25533 zcmeFZc~s5s_cnf#L@0_T$*y|59<^V&=Zf$=2QTkgE;F@Q}OnF(>z9_Ew@EHm+{= zPRAAGHpnT+iXL%ycXm^imv{Wn7v!8=?c^(O=+@vN^PIQsb)!(Y4v{}BcePUNDHLlN zs;-WSSA5@RZ}-T~w$Z`{G-w> znO)MY>Y4gYFLmLqbTKNOCMxJoq2j+z1hVPkU$hD{ZTz)Za`6BC-~S_4!^CcCpqQ)a z)Y#{d>Q^`YY$a~)xVTNKWcuef7c;RJ88;Udas-as=*{{4{@zxpB`cS3WbE3_8#vR> zr#|#@Roea>xvwId=?+ml)P=*?g*MTXOiuPc8;e!{f_z~`3@J?0M^rb z`+pT|I;gpWk}t?PoM0CG`Kn^)i_7x1ZxVIBKd!fso|q?=I{GMJ8~t1Gty?P}JRQ=% zW0+7a%+G(o#G+dJU|64T0ark35o`Ed@tceM^V{Y;D41|qzkWSKE=w$#fBr?WR3opS zb$gz)jum$Qb}BfuNw#c@w(U}lF@aXS)~+2Ff3OR`WaZM@9wTSY`1(m)`dKc!E-~S9g4GnF`JO@$|A*q^J$#=I8xyn8H!u_Cd`s9HlqSR}y;S4e9 zHCt})te*bd|EqkL@!(SIlAlN0rYwj9nb z0V?f9$wklN;$*$X%vDc(TXIl<%HU@{@R4h${%DJ*rR(koU!Ir+?(rY=eqH(pt!yO{P7Sj8e9w56qZyghP{U7_Dfjn-yswer&sB8T zQ;S3bwr!|*HrBk{eF>E#Qi05Kpl*+4hTS#Arp1?CsBaEsA6zuxJKW`)ag0AM&eVIg zE|sAX&k`<{eBS5BlfXGQ7um539bSEbVtL=}d1j3!^FhJP2ffKQTpoHLJGe)OoqEej zY}32jq3z>*mJ3ubiLt#PL%BazrPMM|`p8Cbsrwau<@EwKKfZtZHJ*_4v|`puD&r>y z2M6WlJmsx6Unnj2_m$2}wal;yZK2OPgxA-dWMgC7@cQM!{=~B;FD}rtZf>B`%&nQ9 z4A19ynYuzrc)Kmv!Li^4T{Q_JWP0b|{Y-jK{jO5Vx>R~5bf^9O0H?w!#jnfgOXdL@593CcojD(IXNTW``-6?Bk!GmTWJtF({v-)|bWeyDvwCr!34))%nmj>)jPLMXL<2J^J4JFx9fq zj~L}5Vw4W?Cwe~&e%S9cNJi)RqiIcasOG7OH--NGPd)pMY;7g_3&(^bEWOXquXvqm zKR^Bhg-dtk0^toWhgPH-xy`|btvYruovD^R#PrT-;fIpg3V>vOBS38!eGoK%|Vc z^ryq#rjKghKJ)Qhzh0)$>en^6vze*Uj=OdDywm1PURuq{c}e%8$M=`{p3OHco}JX$ zN>9>$0(WN8cj@wFp5%`EIn9ir+Ki0oHTzx1zPyS*@nLWL-1vgCpFH0$EPCM;|Ab5H z*!UGO@;--0H{J86CwlXzMjMw#hn9sbcY2iX;A2o;bKRwkKYsxJW%`xmr!~AC12Wzp z9F^FX%@58S)a&<^qiVU#ixx|*z^XN$>|OKW{{Ea#u?v%L`_D`ZdbOT5-L2~PAS)}9 z$5{EalEtYgJF9h}ccbqe`{dqg)mnR~IJYBIEq^dkY^nq+*RZ0rw6yZNno>qS<5q3P zaJ8DGttunaeX^X=y(9op2?q}y{*cO{FMxfYM>bMNlK`mG|JNjomS8dR>j*RxNArF+ZD%AnF; z9U?O?;ZwUS1I4C-eE?jdXa^2d!c@7FZ)at>XRb56y5ZB|JFD(uDNp|VHaM^<%=tx- zIQ@`tNC-fZT&|5RiX-9ZKZ#GWT=jnMMwJ#{0s zJHug2TH9|Zhx=++ zOrj^X?$s7cihhV>{32|H1Nq3En9P{U^{CvMF=lAct6TM~U z`7B-*{Qj~3e5#nV7e>mk@tYhhH;mq6YNG1#$tK3oR#jH$xpGIa!ml5B{n@{NZ@#%G zQ23-&aUfTUOGn=`^IKwdvdoAfzQ@EQQc7sJyC%o|J-3jm`^THJ=y7q>oa3KmMd-Dc zFcpWp0DD8j!`>YsN@nQ?bep^-cJ-hLEQ0?2)U6KY{7k;?8Ik0b{;C86$B?Gx9=hD*BWQ@e9 z&k^EiMcK*jBmFH2@OCw|I}MH5LPXx&*6MJ>-hS|6%4*~XQH07v#yne1mcwja?>GJ4Nm)moe@p;$G~ zAG^y1or_px_h(nC-85kEz~P#N!2S#^EG%xhmsn+BC!CsnBjQ4LNG;K(na12-dgc4~ zW1O154%$}7&nq%i`dqRmr>TA6O7Y~4urbpN5x-xjCntR^{HIRf=v?P8*m{|F4Y!Z# z=x0w$gRKIMwVGF^##$2AS(jajiYm?PFL-@v>1wZ;sWIy;zLGSQ|{F&-SLRSx3*kfw$5sH27sef z$+>wvUz?x#>S2Mlio^|Zg`kkt(9^a4j+`Vn2(z#^!{e4?9ZL5~4xQc0< z9Qk;xvLj5ZI+$O9a^yoA|E?6HWczn_QbT2^b=5d*jR)%(ra>A){RO`tByWydZ(F&{ zDr4EIt)Wd;sTWJF;25rs4|lV^$+R;r3l-|snN6=H7>F((ov-o)S!PcVF@h88fre2hyN&~SaahTk3{|@7ZsodPmZV<@O)zM+O zy){fiIE*JqH@7dl@oUo)w>KFGYe-~!B%`E-fN8hha?f&VX|BELfOTCU>_6#paO!(Q zr1Y+j_9@qA8HrO(i)XD&HQN8g)#jGF?15b8d(0^K9UwKD(==~r$n`;PI|r}4#e$-n zfx;Cc(*x`?KdAciVKyhK%aS|b>~7zR(J=@-lkGe1Dp&B!3MXzW<-)?{W%DHC7dgvN z8(heDUsOb`_8zWM;!zM<`fTK}L!C@UensugtD;(32>@Bb0hN{8C5+;?<$3g-Stw3b zdpT>RwBspkeo*b;^zVMB$4t}r=xf-kYSOViUOC+EhWDHRMaPD<_O)V<9z7ycX{^DI z?)#2?_6%a$Kcc!(B|MtRyQt{s^~XN0jh5XztLW`kk*cUY7W`yrei+(g3mg5d*AyKQ zI2{|aqdDtjUPrI79%y|Y85v1g>+oKRQ)_Y{U>CJ^G$UXeBMLi^S2D!da+%kWIi^CI z(&-3?M{x>vu9zMxG2Jh(BJ4juJyDb@X5R=qs}pEBH6{ditar=Zq@?zyy9d6g;MTRB zqU!#&NXWBE=h?}h%<%(=rRx0#OjU~Ox%={l&yP4W3!9!Ds;lMXY`?y_DQo_8NdOD$ zoVD&>-;e=ilL5IXMuyf~`fIqX%1&rJ`tX^ev=GgFuqP#fOc+s*lG}D+`PX+T#u!fO z5KghfxuvUjixEdy`$1#4QUQy(H~=U2j?r~}#0yp|WEAXeNi3WKc2{ku>L}dT63Ix1 zJ6`J53zeF*{rog3h+JC8tX79_6Kg$8kBn#2rRm={%UGEaK6#c)^R9Dn@@D-9ckf=j zEME|d_fecb?6q_G@uYjsa(%kYbIh}0jb(xQPF2mxfnttwGu0%=oFi{n>IE;;k>y>r zgkzoGq}R!juZ!JfsSPsoFD5zcjH`W{?bxW>^Uw%nE5$lyf5Ng~ zml6`enL2NcU6`Kw{qE_BUaZu&8#wg}FrD^6{kJO*J%C@K67RkNk?jv4Wy-| zZ|3L6&sivN<@$9)WH|r{H+{x_oSwGwZx53ERrkd;|EY}+3#U~H5J>PH zqMo;I$Z;|TvR58?vw4jWtqA(c)cH=l%9bWQ?U> zT+gnQp|akGnT)%00~F)pzVn3CuH{`dD{cOHGyb6TwY=7!jkj8OIJDf@RjnRznw7nl zVR|pp=EgefIb?(O9B^SP7j)Ymr8&08@>JBHxc{>^rZ!^<`L|!&5F{VHW|bs&o6=h;6>YA>ao&Q53(OWQp^O(r zgwHJIY{Vl%cTm~qZ1gPN-3(yn{MyNjRFkY!(-M2&b^QNf`cCAL@Fq#VIE__AF7m6++D z@_t-Rv%c@!QX|BEgLMvGcSncycww(xwZ>SJOx!|gd3n1R=jP?(XkI(zxIg;Pn*G_b zJ5}M}Xl;KJ84@ehxcAcn^bHL76_1&<7yg7@UtRq$e|@V7{V>Z;p&@vA*Xq4?Z|^3Z z>*?$i++$cTJzI`lc(#%94`ty~lY>GHX%_qTRUpZ;Lvp#Rtv+H+@xJV1d#zTD+a;XZ z-4+`vq$WW;32nkG`xyUOcCI@~NwWiX-qv$L$Yzh%rE1rK) z*>IP0Rd+z<%EhC|_W3OQ;($CIhuxO&@eP-2O^371=RM}Keaq`~nOy`Lw3OW7Gzg3= z0H}BcB4mN~?%li*lE$MYs?^hu0;Kvh0RNduB43z-(8Z*IhT#C~m5UpMyhLaEAJ=Ew zKP&?tUZ!BXy5%LX|M-4Ni=ipa>K;uy>^2yc#PAf1d=5L zMb)@-@tpDg{9-)ZvMsjrb&MOh$6@eUj>E&jdTlN30;fR9VB1=-Wwxsp z2C(1S*;P2q3FucNVflMriJcIyF`(xppy zd4Ifdi;0O*Kw2ZjB`tm5#?B6rW!a0ipH3{oKHt|7Zj3xJ2v4*d`t12mi~DA~Iq&o3 z++mr&I>MLP zHoT^2^ZxG6V1bR702Tn}YmjGaBg;D*Dj^+$bG4gS)~skyC-CmMQ0?2!v1iVlS;I^6 zhDVv1_ALdc&Rfc_S@SvR$}uTvJQ<`N4H?J7Lv)8T$g!O#dQyfzT8AwT(c@P-F1YK? z_7!SsYG04gYa`yPi2=D3fZh#_bb255? z3&k52s_KeAo>lOB`Zu^wZVJhAFRgyCP5lO9cfzUBy#YaD$*bpItkvJWMpbkDd@1QQ z3so!PTUjc?NI9Y1BPOqBx`-G~I&SiPY4K_2`xbAUV;VD*_qRuh_z^7gQ^oE#XU>DP;^ zLR(+13Y(pnn;Q#kMEh2-k!o5r>sD56x(+MH8P1*a)*bYd(lx5Nfptw<^K_)v0O`;| zN`l{nG)}28xcFpNGWDDX=0~@%jL(+3qz8O54-Bc}PB`xR18xiB;^HXkh{KgvH?Bq? za!@1R?}uO2Sao(N@vPXOlc*r)e!ylgU?eU+OJA4vK_rqdkPOb+E|!{|iCM*kI(I~9jH z2bQco{4u>m-i69oqw4vi5{WRowz@D0=I}#g8y!2;M3b*$bDg`BsOn3@hI0~&k0mWp zR^qPgd zz=E~GKg(O@99rjmOl(ul4UH#&DXjV{sHXR}h6xi!iBe*<4!~^d&Ye5S8qdMjJmxW0 z4~H?Wo0ls{)8u(HS9#^Cv!B)6ThF7$)f1JJ<%*O(9kGcu9f{`gygtSD!9>j;;U@dx z0!Qa?3cduE%H>h!GmE&@TQFzjvhK(G)>|!3^=))gm6GoJbEG$Wcqmf5y}i|Ed<_eh zrX^j4ZL1!7d1VDD2~OVkJN4A|@W9E97DpF4OAi<%9!=29kB*7S^+s{&8t}JUXVe}E zVdJ><=fw@8XUA@%84`Ho0^VWh$BXmd-`?XZJa~$F_gh`XLl$#|^|3n(H9ebL*a;9h z0<^c(atqxd8nJop!6FI;x!E!`4_Wx+=NITE*1&%$0H&wwhldEgOn!TR)#3{*BHxSL zqA6Ugfx0UvxsRN9qmw^2{d<9kX4AEW`V0lkd)x2PJ{Gg5?$SJxMhld~ldj4v@yv<+ zb~o7|X8nn8COj?B{1!g;0!kN4Ws31JB9$bVz@Fez zT-=_BnYPONkI#RYKg2=lYs~3JoE=%_N}b2MNIJ+mROP~v4f=%2QulU+(WjRE_avGj zec-N~c*Y^EKy!1tjO6Oomx*fy89}+>H(?KCx)gzb<9Rk+Mv|Fq-Pq2Uq=tK7@)iiI zD-ZsP-hcd1>7}JC5_FSYX4*+&>Ti*M)&P~%fGgq9-nnxzGO<+??^X!Yzsa@irBXp5 z7on!tup6c7#jEthIKTf7SHo5{Yo&ObFh2Gpr6cRIU}}D}Cg5#lf|eH6|IA^QaG6)+ z=bG2mJi9=pvwrZ8RY0bUAWkBb6lOkeEMQx0s2V8Io==&Q6a>-jv1^}mc_gV;cO2bYPhUm7xj#wF8Xviq)&<2Y|a-@77gGtk8&eQ7WBKof`Xcj#mw| z74wAkKkb!6F^c!#N^0FJvO^0%W)>E6X~l!2!2IR z-GsBG$nJe~4n<(eRt)Ow-zC%!NlTM`@N9G+@=g{nz?xSzHCzB*3=ELD#SA~BqJ)GB zDkm%pP~6+^HNEVZs`OtPl99P-|Ttntb=J z6b6tDy^g+B2md02i(+iBwCbDKVhRxM84xX1^){)zkYbQmw94WO(%eC7PmUY`zGQj% z`gO;X?i&Q*I)a6#V~5Aug9N;*=(bRb=r6T7W8oB0Ia1rI^io~chU7MZhP z8AW^VUVfbC+qt>QA9v7Y$Z5?OTySygpgyf?t#hk3*cGlKFf>oV5V9Hp=KnMJs$CB) zG#(&`f~Ta!s<=rg*i|pUASy6`M&b_`D*w&eqJXIBAH7ISDWCek+&3(lFq0% z(Dv;Uz*6n+__y6vibyjNV_1vm;s&vrr%nQjIU)nne_`z#8|6*rUNVG2wWhsD<&1La(j^kL zIy*aID5(|)GM{`Gae$a&0%$cVQL8wFUH51RdV6~-xN3?Q ziZXFp{(i7WN6>ZRZ6R~TQ$3KFD|BD?6i%N$;WPPb@DCiS1vZeL7juwG>vOA+P9aP> zh2$(+e)$jEXbZ^_Iir+Yr`hHld+%YvRd{Tcwf@t-H++X)VTQ9XLD(zZAqDi{de<*6 zOL~8itfam4GL9?rIK$yGo`Gw89%pxBUYezzn#XrD%n~lC&hf89rS*A?ezJR)Y`{m^ zoo4nN)v9mo`m}(5``gDGq`OSxIsHB6)et-<-@NHvQ zlkwj;BTnt0Vn841TapccyO)0U*PDSNWiiY5TF1roW=SJt4V@t;N-iYk zg(X--%nLjy66LwdQIR*u9GA;Khpg|R(REc9lFRklK$OG*X{sze_JfAS#xnkOa-FBY?I^ zBTNIu4%TTv%hFw`P&503|E~=oK2S;%a+ZCBAHuB%C%;N7NQ~4~sR-kT4Ir!<34UyHcNYWkT|8AlQTo?%!Na84PUx&d5 z!T~r){{471VBG)P!jj4A!_kX5I)j{bsa#i1*#Q;-+3z-cdwVQT1F=vNQ0=WZUs!Oc zMq_MtD>7i~iIE=iaz65OrInFlj^6o5GaBLfsVmEcJOxRqm!dWOYZY+Fp!zDb%An_P+WD!xPqS=sW z32s%~&q;lth*fr97NQ}oDq^uaCG}3D7uUsiJ@lmeJv=e#>pqn$)JctmEeg3H0%bqEEk}krAhc&uG&Aoa5iY z21!q*CNY?Y41O$YO||ClW6eJO8oY7`ULqeSMMt7U$$iz&6X4nG9r!9jV(>kv#=pe^ zclp(~S$BwBoOSfHaaU!VQ<$bcRaOs~Wp@$llOrz|RjUtg7ue_!`AuYJ!wR|QbP|#2 z*;~Z84~=|%d)Us7^-4lQ4eVjQwzs#6|5UGKkJ}p^_x|SVd?H=}H@9rbpPfdTewY?) z7x4RZYLQ0Ks-pNI*WKk-`K(RL!!m#S^soDR>++GhN8+GAy1_usYn6=3W_nGWn16#i!w8oC9s_J zpA6qBX?V+^ySh2Z{P6{H{s4xO`dnVF2pni^nT`fi2QOCn5CE-xti?`ltpaa{()W-1 zemy&VEg%^+mx|Zp#gFStTat1m zyE4op`Ok^qg&;zRz=32+Mfsc+2WL5-lqxA@WgkKk$QG&FfU+Uedoj{f+`625*W?VO zm<6-4R&&piDDBu_mKUwV%08jCeIr?As}2p(0xtu4b`r=bH~%rUlNX8)EOw7g*uZ*c z>uvR!_RH$1CF+!iQ{XisnUW|Ml!Av*U3c@}uxg}O518rdC*@dxkUGLBOWsv!|P$tX_9wu}SNMSA74T`|UWaP+=nJqGJWo3d}Iy`@r zC3Ie3oNv83crcvL^~?KNn2NHM5t2q?lH51~e5&L(8 z8?JWc^_A5oZZ@eM)6QVi1^4hLFySChJQ058vujb%Y@ypUaEziPWV*s5+aD!@&qZN8 z3stv?FQJ4i-w2ZO&m;}fG0BLCh~fsbV6L;Aw0UgWNEOb6-c*GugFqfJN8-rh`1vPS ze-))hQXbkIcHVaU_?w+3<-}bpHnX;K%TjA=HXd(^g=2_I<>%ixaj~(Gr6nfDjg+Z{ zwwz!ZC(t=<{pJgtoe@K>)0SjcUBzC62Z|V#hYFnyz4f2>iNDKnvLy1bq!2&5_hRz{ zKE0LNEcZn0s}vnePzDfAeTD4*Ldde`nS;ts;N|Svi|X3e`XJ3j@+IOfNWf^vx_o+4 zg)*qr9=Cz#@iF1g8$kYj5pJzB3$|CVy%HY2`0p1ndYTPLtsg!EI@8NJ6(qi{)f(r{ zUCG_{V!mAK-P@rYs-d%i(2|hMCRKd6_6P@s~aq~V{`m|r@S?-8)|0= zkYffg-r@@jJ7RB#b>+0Jb(>YOh|Q0WW8G@OuC{XF+Xy5q;^B(_!lnIwynK2z%>T_Z zPp8mO-Is6%2B$M2&|AzgShC9)TEUqml2@|HezcLL&UW~izCu2Ka=pgHNZVcKSV@}u z=gy*sn^#Qp2Mv@&7_UEA6tKj6FGKw=qR7%f%&NRRGr=c$!*_iOYs$GzX?O1&Ej=w6 zFtuti$JGsOP)UDDiZEi_xkpula)Ozu)Q&p5No47^^jbk^&4(N9UrdM34SVDlrf0f$ zPP`z8i>mWSC%@wMFwJ)G+-WaXZ!=|Pn@9p`N4?-K_*LDdlv;ch0tH>?Ue=tvVhQQ1 z9ZMg`Jc@7LbyQ=gt0vkrJf%vO8(t+T?a-H9#u>;vO+VF$nzHxnt@=A#Ldanoxial) zj3)O&)p7)khVj%?Ka`>|(x^|DmkW11Jrc>1vFy}o!phw}(;dv3z{;jIHSDf8Mu7-en zNstsd6`9iFpHTZ~{R{2#%I#O-Q7@KCah;dqJpXHhaLPbR_p#5n*aD}AVz%e~vVtZu z=T*OSsv?0@1oUTZOioTFM7{dN*V}1(Gw-}u2Tr2#dkutie8`yf$sdrIguw##v~GKG z9yPt4G}4{$@YdHz{!V=R2xt-%D8q&z2iUcHceY{6QI>E;CZHi%qdD?e^_j_G5;=%& zF7JkQ2TwX`@yv5$q6FF8Wg_6Wc^&OI=C{+jCP4-I)pOAzAE1<)HcI z5KB5_Ts{e$J$8SAZ_?8g2pcAQjQxpD@BKqd|Gb3q&ONnVZfpHTYRYd4#zt)YJxDw!Ym@a{3+mlwNM1Cje^~=rZ-EXe zTlquh9-?nyVovTNBoY-M>$t;=ReQuB(0U2@XWr&zTtt^4WZK;Gn}WElkYYYfMd}D2 zfhy0OBI%U)O>n;$k`1P|VASjB^XwN`RM4l)#OJAQPSSrr?whsN4LK z`fqjj%8_V3fvVIVziY3#4mON1V$TUjl1HSLFnN}{kgyWnn}UK{rpq&*F#Rx?TjrTFcTpT7ywu}JDUEXh@=pIOWF=)<_%u*l(i%9-`f~&8XHv-8jqHJR zJJ+Q{N?xA5_5Cj6WXO4tT-Ac-V$24gOZ!Y9JUGZX+SttTseXSE=atgW$TOJS$3mj6 zmVY+FO~JAI*dCro&qi9v0H2Gz7|k2e(9&oOdO<-(RG*+8PU3h(wZiA=J3-|$k4IvM ziOFnjq71@ON6M0nw9aH6VdzFiF`hxtr?b-Ghx13Q2SES z{GW+HsQ?ND)r?xQ(ahJUEZq7-@AghgwQ|P-PGl`eS$ja7nVtbmk=B4lJ?LS<1V;cQ ztzqzUYOT7o2_S<%W78tyB_gPyKq6yBGJ?a0kr3(ogQMkU(grpl->(Ti@##s9c?5Y4 zDl!bU{|pU`s10Z&c&<4CmXqy#L?w$={l-b=MtJB6LPPCE-JU|~R=))+6`9W}mWCV);89@)Ujy8ksr4*=K=?3!8 zHh_a>l2XKP34?OgBuUljpCV!*P^yBw0Br@h!kA6?5RdWk@I1(T6j5y8gg^;#A*!8_ zWdN4SNcJR=KCmZZ)})|^_hy~`_sdbz%r*|`KdUwv3w75$j6-Bsm^6kE@zwAnv0m!F zznu3Wc7Rvc0;-TXw3&s)4(fthVGsxhQUQIOs& z3V`@iMI5}*IaPaXU%gG#}Q~6_i8{a`l^#TEh zQyVo!D1Jv?nx|f1+Ah!z!<)1f9C!Wl!zRXDkEkk9xnBRzG##QLtHrA&^!4>A%3ZP6 zU+W)Q5^f%84)fU5$%5Y!6*)Dsm~wyLV=L4rVLB#z$-4}P6WipR2g3rm30$t8J?fK;a*r&%~Gj;mRQ_1lG3^!DY`t5_Ddx_V>)a-r8<`Am$ z3<_kX1@y-#ow&GUv=@jrs#AMNx3f z*Ft?0))YlLME>}p_rC#pasd{2!fLc;d_lE~z-~+-B>m$`9>XSy{;tJ;MuS4iQvim9 ze`Xx~yPc8y8O+P^ z3)xUjfT3X2x={XPfxbZUO^R^>S$4FGrE;UwFX??Qmh%_%8cYm8JeNzkX9^$!<5X^?K=V?gKD+>cpz%xZ@|oYyQ6$A5!I3a=>SH~mkmxG2 zf$*O~0wiIW$uTgO6=e+Lm!vY59Am+F{|m6i>9Xbgz+$2}H(hE-|8@>1KN?9nXHcI|@&$XcJwa1r@D~IrNrMTzAr&A&Jjp-`=C9e8IFjU0N2 zbNAMweKLojFYLHr04R@Zke3osj0_@QLY8BrN1$R@-q>ckOh4%#A*CCb>v4b>D9EM7 zNj(_^Qzg17z?B99-I=-wQ1y*b5+yO??G`>FD5mf*3zu2pBn18VoaZz=rt*6QHHs&! z9FAxD1=Oq*3{nX_Ggj$0#gb7>n22+{&IZV86LsuhOw!X(qoP=Pwens1<4u%cj~s$%G$Iz#lS?EMfZ9o-==3}eTE6?WeBtaw zl&9nUr^n$kmMno&V0&e&Be3B}YZ;ZwIQJ>jZWuN?YX|6D959kiXR%F9{Jagj-ak39 zbE{A(uvq*H?CEb`$UB-)4GJnJ1$>{r$9qtOB}HmS9hCME4jbF+jWoxVc5IUxRLGG!8CI$-WPC@ku z3x_1w(GdAVO1x`@t$#ss++u3&Nv668O(N}sFT+=vXUoikE2EO;hHLS=T5fOU`s)oC zpWsgwRb0m+cNyJ;(1e&evn)?=rIY{f#u95gEiwCE2;D2-#8{4?It*W6fl67O7&(c4 z2!;_5RYNr}xx|>By%N0%;KW(X{11#nd-t-kl2q-kimm!ep&9aq=n-&-rYbhz++_3v z5HT)kP67)&ZeU7Rgix@>5h0-U`B_+oAr`Mrmq@z7qNzjLRCz<=nzvi`BQ$<}h>SE* z&bF|XVE5?RWYpg*=>A3rXSMJqH9{hFp)7Tyt}Jgy*EOd*cOk-VYC7Qe^PVL_HO+9z zrSgu!#Le4u_LI8I9YgD~k)E{4Hulk1m0~Gr!GfafM_4ydHfhgG7mVdvLSc)G^W9-# zbavvWO7Hhf^kdIOo;Cj)q+Hs>^P+C0LHYi)_rZ2RbJUc{{ux3-dYyErC`_WUd%Bd* zo}1InZ$pJ>VzHuTO?}a`qO9Nr)B7!q5}KbqIa(_Q%7&qtC04fxh zzO%2RXTLQ3%7}mnKSIK=J)ru!cfaL~m7aRF|LvW}4>i7KFZzLAYkBaa?CBoO>L*3q z7u8$%tid_BSNk0;iQw(r-We+Gw6Nez(~^EOw8H)6f2T z+4NCOBxxf<0WZ;>X*%Fx`S?;!#cLIHw)*cJl^Dhrd+Bl^A6XK>I3A#b$O-VZ3AiJu zob)o1$O5McIg^Q`FQlo0r+&44&$L}Q6QpN<lYBg8DXOG4Z7-{%*bD{bB#FJOqK~wc0?}w`9@9zpXIMRJ7@7aaJ zN?#txyl`3m>D;`+qy{9_n(YWRvqjOZfmVmtfk}>5EIapIQ?TRAX?C6Of2s7Vp!>3e zdk*Svs8FcUAT18O?t;_?3BV~ExHY6^%(MUgzo zzQs61d=L~i+&Z$M0-^}+&JOrR@vVX`x@3E7A$3cJ(x7(zs`lqQ5{5 zOkvg*(lariPi}T1{0T%}I66ei{1{Y+eNPmhMeCO#*Y#&P=_UMcr5+ZJOW~d!X{bZ% z^I=Zc(V+ry>~}ugQSfpWT7r@O)=AAwj}>|&8DKdA@{d$6xr4|Ho{|@!Kri`ydC-pi zex@FBt2-jNv=%^v$})iNQ@1}!59a?T>7myWEw>#KoedNARj!V2=1NWXco|SRXn%7& z^E}=FqR1)cRFj25g!%OpG;p7M{B`>3PqT!*^fRO2`_jDztte<oW;JlLB^Agh5 zU4?$9KGfY(_*T#IbNPn3suz+}e?TJ89llkV<%7r*HHb$LDNTsLQ7uGZ<1MKJKGP0- zEDJpwq;V<2xAcuiKdkke{8jQllcgkgQ=jH!Y81pvlA5yeDcQ0P`(op$DN|!y3M7b)n~)@W@b$Q$J0ohZANf zy>?eLT=&pUKndID-uofMS$Vr-Zf4mmQBKsxG_XzI5|LM4!d6KLTY{}a=v4=t{01>Z ztq~~Rp7#YBPUQJrvv9Z|tm(v2w*veoRd*o+n$Qjli)!?5Mj`RU5=cIT%P&MZzl(4TPRZrHdAvWTB*D(k4(v5MVZ;#*jNpMB^rvnhiZbyXgJomZhbH}qAbMZ^X z@qPC8((`#`NnaztR;EK8qpCn_2?uHKB^N6|<8F_hQGHhf#%JU#Wbt9}>?Fq|oYRey z;?k&^>+c6IQ-HZLJev>cc&qHyVV|H1D$59V6?awbMR0(E!0WR0?YB}Wu1Uzthe6L% zqB5mbPg>;oKVd=V*4cc>Yll-vO4-*b;-+6~7E{ z5z2a@WhE0zvq%(4E|eDz%RKywuFF4;_AD8}dervF*wMUxUG>CJgdvqeI+Y+7+C`(y zDk43h6F=|S4;TM2v1a&(%DdsKUa_JHrPCnMKRP=h__z!J}CjkOMKgSsAC4szegtt`F}y;mAv7DdaM zXFCBQj?OgvgCA&6uk#yX?u;&wsgP%PiFTV_(1TtVDtBjAf)&h#hw)HnZ zBby8v^kJL2$SwACMkBaQLIXjo%2)qV-D9sUR73#p+5s4jZ@{zmHR(7hnwU* z!>A6N`dL@f;ex)QT>M`T6pO@E524YH-_m!GZR+=^c$O8L!t2*PS8nzCu}{iyw?(AM z;x40m;PlnD?77h1Zbhnsc$ZsubtekUO~O$ZjP@L%u^lCv7C1%D`NvM^d!H;W4z)6Cb0JT^%<6vF+h>sHDa{8~wZJHb~y?@}_+z5pfz3>Kppy|F5D%$-?|?s@R# z(UHj0>BU=;7I`ho-MH$k2$Y>4)PA(0pC#?BQ&Y#FGFf1VzQ1&K4&rE-Dn9__OSCZ9 zBoO57g9W&@#l44iVF5oK9uEK^1r18b9+H0IRqvoS8!r}_E@D-%-SOzS5Np8mLFK8_ z!TOq87sPUHXA@o+D9sh90{G0Yb!bz_)t`6Mz*}C0>A}p2n0e_lU7S`(@+RKEAwp=z zZ1>$Jle*Hz#)iarW6kl|0g!x$zLv|Ag|aXUG}UYpwMkWCdqJ<9=C3b20_TbamvMyi zgwR>DSOh4C5|^1(Pt@*Q#HpaQJmUiF?9WE#W8VWGf zAeUQ?Z3pf_e!NmiDdsGv>P5o%T`{ShLkd+Gg%|~Q6&xD+@(R*yQG`*_2g>X~ed5-= z9+&3erXi_Vg}^Y)-)S%Q20YvU_*VCG_K0_xl6d4TT%B_Gu!%==(kKL<3t^+;a7$$O zGP10#(er^owZ->Aury!Fs$F{?e!a+0l_Q-_&dfWDfXG{e8ZBU~`t>rw4qH!(^l=2ZB9dU||0#Mr- z0Pv++p_)_Ig~VJ85g|W6za&UqD+fC*7CHtzb97(-?Qljhy!0znKKAAu7ozx2yh~92 za%D3B9Cbg)-bik!f42$t=Yi8?dNN2^d%#G5Uc-GYUan!0x(GQj7ANi7r!^$woo6A! zL*gn1l0uhOfl*!pjRPT#F_sM$cP#{W4{1W;BSwsXOEVU#Ch34AT9qQ21*U=5SksYn ztwUG+H)It6pA9(BN!s_#^P;hv*WeSFH|F5BrBAWj#9n~XLtS0&)WnF3W8Q3XNe40^ z*50t98-LqrF%ogmyC&A;1}1O|P;Qlf|C9+$N}a7WNGL=U{bN>6h>73M>a(ntl|2mq z1?}gowO#`UN%4yud6{xFb?aTyyDe*xubdHd3LR}rLytmMcq^E6ObPej%i;?wCs&ch zBK@Gi9<9EA%$WWhL&~P;Bm&`yjtwmN)PD(R0HsNacOz#JA`LwhvlEeZgUh=9x1n4H zw+f_w*o(fYcHR5nWfNbJyLG6Q!326$R#q-aiDWc)C-_&3#~j#%ouDW+d#yrQig4zR zxLqY_Umq9`Yh0H?w4qo56R&(H>=6uU6r>#JTMAuY?GDf?uKtj)rLZqpRs>r(i(3#H z!EZ`Cq7HQxCy{fX{GR%PawMDE~i<# z{^=+oV-Ug^Oj2-|NxOeZ&^}8*+4CKvARKi05esi`jh9&ju^MT;fRQZ^AahD~|HVPA!@{^+v(@8n3P8C6{cFI}Vu4-T!FAXGBB0%Rp~hH>|}7{>Qzi z7QY2ijo&spX}xH3z7)Z@bLoFva{{3}>8785FDVQ5lH@RevbcoN`w6(s)qOk5@x-C~cH_^$Uo(LDuW`VsyYAVng4kn%*h@F5g4{Y>Y85hKG2Y9LiV|vS z*KmPL#IKF60T(LnF>Qdc_?)*Dm|I6 z?c%Zy7Z7yeMl5*>R12!OMM?(&So$#xDbn`;r~m7{LDa(t$NT}f+rSlNtXy3q+I3i-29DCV zix7qeiulDUH;;iwVmLRE3tpsFfq)!2d`BkTs+0rQ=d^=I_;+ar8T~RM204pN5g>L8 zwhv*3|d%+yrCvsN*X*Py>W|WF#F}dYih6sK_*P1EdN% z#TT-gthumkkmcNjz;}O@K!_gc1oOuIXOyokg<9y(4JpNZA&?OpL|!TxI%ZBF?3BZi zrrdQz69v?PN<6uTz| zt%*~His<+&+!Ii`1viz=mw#4rI(+VCT(CMjJ4b&F>mAmy!-oePWy?BO=@A|*lMqB; z*<{JBAB=hO)W;B3&`qRCVF+{$9KkxD(eEV}zJ)-Xf>eh7v80*YJn^&)O(0KDEma!N zgg=c#OUEPRZ$EZSFN7tbvw2aB`AO;j)!w;3HF;)n97Tc##EM)MR1~HKLAgc2vOu)h zB%^{^*eExxE;n5<1j@}+sVs{M76`DWg+-LG(5OHKEfN|)gvdp1F0L#=(11#-fIx(B z-S0!EGyNBKW`AOs$>e?W&hwn-ob&yj&%63*Nzh5h=F*<={jz&j8Vk$iA5k-aFOtrV zdZJRkkF%ob(WBxUHyP}AWI>!RM@REPk`~rGdLq9+M@B^(N>TL0V~_8eoxo^Yc;yQU zCWfVZ@28}ssQNG;n+B0A=K4o5jS9!!>ml+xNwqz0^P|F3r9?<1F@%1F!Sw}LSG z9Siys%0)T1ezo6MBV4Ced{p{)ssj+vSHi7ngMH4Cv1aRoX02L3NKB2E`Q!5PSj81f zX%*mc*skomM}9c&aU<2*zcT%>^}QY;j7jlRBL^ZPB9dB4&uvG)9MfW8jb*WdF3J?z z)5_8~WrN|j#wnd4Ip;5nBeb~s#P$JzQ&|F=!5$+woJB?kFr;XPDgv!m`0jL%Y9;L< zmNYaVy=PPedbYiC;DSg&wqFC-jODefde>D`11S5O9v!QbjCLUfNqE9w+b(^dpCsCb zEMWZ7YUNYEx8T$8F^JeZ=R&l5X;9}#wFGZH-J*HE%@&&>=YspLtn6@;|s%^{x?(Gw|`+)xB8G{RH% zpeCU}yg9_DYJKH4hs+&Uw-eQk4)9@?IJV7KAktF}B(8}r=Btr11oNxmyvT=_VxBW= z7^24PH3^;WPV0wYUn9KMLYR96dd1DTd>>=tPe+Ss04Y^S=M9hQysNVG&6Y zOKlMD4AVu_S-K7nra?ZB{Px?MPx+fQ$xNs7 z`-kfi0VJ?gyx$-Y)$-w8`_$hRQ3b?^A~*^Opa%q(3v~cWTF@%_YLja59$Q={2NJP} zz9fV}Js}E4)|FHLDqc%O)1*c6afZ3Th`1rx711`y9L^i7sKbuUTg!trO!o@d1PM%S z{oDMe4Z(Kj0=MsMJ?R+kui6{+`f-ltt!rNt5AzaMwOl1}#j{@E9YP}7*8tV|M|FCj zZNMu(l4^R$%gbE?yJ&b+k@7pM*F}pqpL{X^tifu>6pY{&VRB&kTAk$K&H_z>m&?Y3 z)tGqISzwU0CgGlYCgZQ*_pAVam`$j%tZmz1KkG}>Im9%}4|1xmpQ2}o{zIs#tZk_7 z9T~2-8bfPmTEW~Qw-RQdN7XR>BB#5tb;&mZ&T>~iUH#+}{vNwu02U4)&hEnOKHqzT zH9&}njK+Md8-W5afZoC~Mv`TDd-6?p%7xa}{1e{IMec_jS!o;Zz9_K}c+<$DuMog8 zzL=f;KA?kcb$y^{@o`gA&(BUw*2iSq`jX+Vv6@{|v?I&PVs8G4LcRzx9Kpd5@xSX%dG%vjS#MA8`k!`Ly z6MM}9b!=s{+QizyuqUCY(i51!fXr_?hzxAw~lqdR-j8wS&@q+J?Kk z9o@%_y{d<^g#ZpeU$UJIcdfY!Q2LYj<=#4wgaT40JW#4 zY71JV=A_F}+3Gm0TD*kSNypTcLgBC?e;Dp*rrJ>`M+IqG zTa7$p`&vDsjk-FXpTyQ)>j*ZrziuN{e9t}7N! zqte2t5=&ofWx0-ji%^kT6NZ1*zhV?2|J~vL|G)p=UyZwD@6ysuA0J;)t3J*jE@tp8 zalGy5JEgYkI#kL%zRPtY{3%C7wz`XK-6v1m%zbwQxAk`A>TQ;$Qa4mTnm<0#@XSWr zK(&k8`}dD>zp1{Jbva>4ql-ktD-YKRQ)!ZYJkkeVzP!9V=FezD(a^_(FPF2bYK!m- zgfy_do)~(-z&ROdGefCn8TOx^JgHp0e#=&ot%ds@_IOS9j{f*?bvwJt`8_?2!D2Mq ztIS$qNdnO-u44I1glfX)COAy*a2`5ietCgwH4%PRY zUIv7g4(_?MHbnWX`0R+#SksA&1RGktOjdwSn0(XIQ$Kq?B(h|b3@ywT4>x<0VNou5 z>?Fg{9f?{$-gjJ(Fzl(jv%M~qYL{xdg-YXnkePYi&_E-`!@@VRa!C2mS@zt;&zKE@ zwM_OEZW{0#WY~36kySN}+Q;2UUQZVZZLimp>G)vR;F%H>$q}%#^y-ju&aE*<^DE^hPmV29H`pBd`)4DI02VY7 zTN&XT=lPZO)wR_Xw@U}kvgZUIpww1$R{!Z)`)H;i{%r7CT1nN$HDO6X+0LCi59A+v z&Y*W=P+!HHwP@uZn{_V(h5TNhVG&&A#!qXK(I%7XdQ{l|wOLee)xqE@meQ^*KXuM) zD76H{OY2I$aw`*bUdgs^aXnAX|dN2)D* z>yx)ugeEy;ljVAmZTser{YR;%%mPX01;#rw3{wr3Dh5&=Lddk2rR=)3aC&rManXGa zw<|kihLubGup|J@)nL){5)@y;sS?cZY!#k6Tdg zF=n4aYv#*ZB0cNEMi;|K={rsGk1RDl#~vzX9B^2^66<;{ScFz$NhW??|7o9bPlbg5 zL#PqoyaC)f{`*HdhpN`i^r@jnN#pwqZf+0Do8Q1iOZhfhZ&l>OM@%Tcj8oY=GG#}? zbcOuoX_#47h2R_UtW|By0@S4`S|a?-4~yD{wCJX`^PlMqRtu(XknUL{`s;LiqGzmQ zg^-XNH?2u*G34bDZS31yN(v{xY);u7 zUxQN`v+)Gag-Yt~3)@=a^y%t70ixeR_gu=8KA2srF|~l~MJ4t^awXg1#W89gj`y9a zesmA$3+EUjN({ ze%c+si%|%~r%!s!sh;{#mt~_(2B0uEt27cuyO|_+lBP$wy>{8%se#(lLOxs8aZi4z z(4ljZt&+Fue07a{79$GkZAc}fx6buJ60?`1?zH*1c?ce8TQ?I`5(V zkG{Qg8Rh!3!AW@VRg#e(Gc0P;O>^jL`6nhOUUm-k^|vK#J!kZ} z2aaqhpW>(fXX9=Dno)<`c{vzXMr;{#yT!!Md)di8!+i6^dqZM-0>t)oFP!@MQO@R7 zkWt5<>5*C*m8-ecf1r3=M*hX#62F<-8vEsAzFm1E+x?tH(9%VoHvZLt9DqD1u>K4^83aQ zt^_$rnH4w%PANGw<-T5|{qVr!E9wmwm#$XskG6@Pd#9~`x#Dxlr`kO(`xuH}SRL8- zu&Q~WSo_u{hqM2=8p&mC{&T*e%YtP~rd1I5rbr*hW}BVrirhT?daW5_3+sZYBU53e7H3+VqGLBXL~rnte|vM|V$~be zJ}%h0UB1+zr@1;ECfSE8-N%;(mVNv7ja!1>_`VU%rpY2oU+l@;?9`Kvr$6uQH+kJU zq^urL@1k_gbjyHKbkuUzM^7e?zq%&0yQ?blT(Bsuuo|X5X}1JI9$xy)XTp6v zWM^XT@eh*5$}lhOWZlU6XwPwJlgxlOGS8y-w|Smp5?OZfW#_0!frz|7fhhM^%}VOE z)f>M)n;J+xJ)Z5;DnpBr;d7(V=)K|kViP|eWL)769w?dfE1WuNYx@o^%BbU5L(0+> z>-Xu~eEr%`#9l6%uPkBy1F~l9UT6auY!2Lb#*c9 zd9Y8~K)GXqfod<6Zc)z=Y|pcg_G zemObW$HK$QDvQ0}J=ibHuJOn6MaST#+FMak6%|7MMQ=49VA98plD3{(SUwXWn)b7Dzyd-KqOxjz+} z?ghJ*`M;MkaePY(H*hi1qNg2Z2u*Ua1l;-7-JQBq`N-RwpTF5hF0X5&kTyv;D0gmdg)zlHDqo>|78W3S5= zfrwlcaP}6oE~uKSeF{-KkL5_*QS)(cC^3}8gtXGT$f+E zkDq*hZ|{A3w;%5-l`ix4pIt0tSs5l;?$_d^tlwR8dtzq5e|K}9!_}ETVGG)Kw2DeB z{g`fA7R0N7C}iO?#H=-?3FO#n{fH{S3=+i)-zQDd+XZ_qAXycJJ=) z$PzjjmWSv=XT{kz4tYxDPq zZL_EL^#$!&rxhi0sNh?e-my=aR&vSVNdh?mI`xhwS=Kv_yV*>R_WEG6np>6l*>l?b zrjo062C@s>+xPGQU&H;PH1lFr*!UVvP5=CmEVyFV&$dzU3JDb7^^e-?wsT0IF$x>~iI6#}8CFTv zyCFr}m3mXdSM^PlY+L{AyJH9j#~L3AM6Ta&U}J52m$(U>PM_3qU_kLr zk)-<2%jMIZLUVu0$MelU-`|_KbH}riWA8*rEI|Iv-9Qit=j3;-mAV(zIZ)UQu-y&Ie1u;!Y;_B9LT~3nAGTMxN zI~wODhUoM)`;AN|97J^C_HAihc@sU!O=WuXmFuxsZsL@xLi*E6Y z7u&wSy%ojR0YJJ5A8G2J#dG@CoN#O}i|x9}$sX7cZ6djvN6KW2qoZRem;CYf>txqT zr)eS3MUwpoh7~tfh6CGfWz~Vm3V#n&1i!kR?Ls|%TKMeG*Tq<7!=Fi5%8J*aYo&$5 zbc2|+=1Z>?YmaP-2ZeB3eg4+&eD76%$t&*q!~|)@MQ4^E|IiP zWXJ^sp~BG`bwq3WYh2c{hOfhvHa>lqW+?unF(dv}F8%vW{FRrN7xqxJ3fA@Mp=r)V zUWFq`Y!xdle4YpUz5i7>@N7ql(T9fzm9E)|&a@z2^fqP){rvclDX-<=VYqAjwj4y1 zUC2Z8;-`Qe1x_98i?th4b|KI(-Bvwy7n%3_M~BqN&u>mo`wR~c(?yeDutBTj&R$uu zeqpRW-3#ec?i(Bygq4QW-A8c*;8VES-eJ=3d**!#c6;)LZj&@w09jetQ--xO<8AQ= z9iNnQ;oF$-%zYpapg#-$ncwqZ@K=7DA5h3M-_!7baj$a2uDmJo8tK94S8uYXNC}5s z31rr)mz6r0myXjk)_fAqP?=v$Ts&2$W6+?AJ|XAPZ#F;LZ-}jj{|t5*-PAw z70%sZ;%>)2AFYm-I|mQv=rQ-}mwi*7!!1dEoa(Wq7pazdti5d|nyZx@7qqvZ*K-@B*3fxZhE$klT5d(u`7QIRnGO%#lz%cFC;MO?b^ZGF*Di}jp2AT)TpGY& z-;~Rmd0wY!BM)a<&}9I!*@UbQzwjq4DwYIJO&4>^r`!C(j62)L0o=q*l(6w^ICJx-St( zS1CJL-ak)hp(VWXFJTbKs^}7i0FBsj`URV@U!T)fjysT?3r7~Hn`{2G)5UHfcAQ(d zRCgv;^;c$DL2t?}*aaaVn9~yy0-m^b^GNVtxNrd|1KXOG{SKKcpOT~gI&NJXYntSb zoLUW=!S?5AT*J58rZ3)dd|LV2c0OlL{tUl>xf&q#g)je``hq-4NijSHC;Io)r@f!} zl~cP)@fat(3x(hBPsb9$&7Uyyl^8v1bEXv&rU;Aj8u`8~q$0m?&ir(r@!|7{R%I7? z*lD&e#N&Nl>{4otkQCPDcV2Lgg+q+*8aYjN)(n-L7_}7g6msB3Mq+TxHKzxe@E^H( zc(^WW-P?1AafPIjC16Lg@q>Mc9oNIdx8t;)4K6X{ku|SgEL&7+y#LUOW5M8(6yQeQ zUz_`08^2p56cGO!-$qHgGC_%oDUnHEe;6eVYv~91crA9JXK&CtH>r{}E^9>iwFxxQq(DPr;n^ z#s$@Tzzp^^Jzj4d|5`kp7U%VycXlYme-AdaZFWrQ=Ef5@@!S=5LfTuKALXeoV2jEz$tDf7WUlpI*d|lx~dv2{NECaRN0C#qj*;JmW>?yrJX8b zJc%2xk96*5YH$2ewy@oGjZid)&w?WE%b7BQsh^)s)&C4q==q!@t}W#2mJsE6=!uJH zXGKW&ISvgS-+@1!A>v9(`7djpc>g|rq&hlOk{@yLu=ZTgO+~w~`7C*z!dl(1Tyc%b zp0yX3@LIEmf=IgVVmUiK(V}X-U~alwlT|dFe>vMbh0p}?x`I`;hLoa(`U}K%B>F7B z`tEqi%&N`ytGZ)d4Vo;BzW7Z!0N%30Rsu!ZYOAWghH48+_uc&<<5^SxGo)!VW6c>Dg?aI$?fh-wVrTfG1zsgsAT@NYLakHh@8VE%A`PCp;!%e(dL30``^b3>?Q ze8P5z?OX*bdseeIWS=@ArXQz|$T!{-4a{o={02tsbd+G@y}fG%J=+;>NSV?PpSJ-P zZ1&aPw{PG5c~k?CvnP$7tX?HcYx;_4*^O{u5P2PobpuetbX(FA8wLp`aCFMuzj9)f z?YPPUo^MfZ*DpCD=5$=KYCKX>Zs_w-mC2q&kDtn5f8@toTsaUsKRvAQ`1$dggkg&P zcx_I0_Aw+k$ImAMgdsp4cojiSoAv@1Rxt7X*5z_*id$b(WJ`;g-ld4GQfrdUz;PF~ z&Bd|q)~F1>ydZUfz_FF-3k^5|C+^Wjzj#hLz_sT#gdEB_vecvX)XI}ZL$3sFk9|z% z#j(O>;U}#><=4_**`Y07D6A(cMa_U3^YqP;gug>bq^X}4)&_yhQu1jQ#rsePKd3QQX_CW3>!1^r9 z=Vz@oJ&mk7@5yz-^o#7ewn@p0-}8~bEl%V4SB|X zX*^Vccb0*f*FQk<52^67`(b1b*5v(oIJjWV8AX325PC^Ir8iC~g0)!}I{)5f+WgPk zpPw^K6eOmAv!@1H9LPfV%Gx!_bxa(aOOvC_t4^2R*%~OZlhz*X6umb*ecB41{v~%GN#56Pls=vU>4Yy^yEEyE_IQ8mlc86u1z1C?gt5dq zely+&Hl5fRHSny{qHECqt#;}2Ep5+yfkPNhd`zb`KglTg%2uQLGd=hA?y%ewUpzaP zIu`%_XvC_5B6bQRdRZU57sGLygi-xg_|O~LSlS5gdreC&!;`a(D>OE{l& z^WvL`gUNm+r%r=mEYDk03|e9H@awfQjuA=pU%wkIj(&R`n#Sc)PZM`W%6o8Ik_#2Y z-iqJTIkhfJBASO8oR$r~_GsI?XHThBpg>-kZ|9Kb{iNxld-gvYk;|pZbQegk-xNIX7|re4;XWc zMyeY;m$cn>hcli*;2b;s#nZqtYOu=1C{#>TuTaw>yt^XzuTnUoe_ZvWL~85{ln_o} zAj%c;dNIX!_o^-s5LxH#HSoq-=-Kr|3DKEBx4OoHd1YN@YS5`$m=o}-~7)3iSIFk@jEy*(Y1pP>tXR|gB z9(r_|fObhxq0*8qkb_pfeY?+e+VVa~GVAoDqjiKn_7w1{0+vaBC@mU-B z&Q?d>>^0mX%l(36DA|d7kAJv7KTKYi>7fclv3Mg^)J9XE3_P2H8Sz?UR?n`LM=y7~ zq!`V`=KQiF(D2T-3kiujVO7zHOK0G5QgvS&?p7}+5Jm5j(N2>sx-q|wMM!K7TxWHm z^u!&`)~6mEWVM5Sd?{8_`r>JGd`c7LRl?zsPgvp;!JEG>N{JX*Emh2tzMI;EOdCZj zn(Zg>P$o&|r>ENH^3Lnn1~4Z@sAOq#q+HnMv)t`E*XW`>8SW13%IyL3<0%L?P`$j` zu|W7SsHeGG@Is|rB9Ycu9m1m|9Jt1}gk4pKHHLLFC!5@{*PIAHbb zgCWLUm3g}EvB@ENFQ#!?1@UJ2M)7XUgxoPo`&BHnYm#H(chMBrqDD16>vZ$Fm-OeB;9v-{OEul4YnX(L* zmHFf2MKfPclafG>PFT_sIja{7W+xj_e$4i6u&~S;d7(X=ojaW3=3@7ywcoL*#A$N5Tib|QoX9N zcY2fdXwP%zEzQRw0$KAeRWr#@4g_v1I+)5nH#=V9^(}rb3D^#0dTQFeoEp`u$a^aUo`P0z4HDkIejFmc<3 zB+GK^rN{Y>nTi=imrel!T`_(@_y4$oFWFXP>qD)xY=D*&womdGPVC*s6JKG?J^N?> zTDA38_emdc`YMrHN9q{rqkC}rDYd#`$m4nvLxw>cq~v8h{UKSS+!@r)NaHPK3=`g$ zVg44(Pi1YbxTa?O#7Or@*^duCGavT*jC|P7$@cE9t=`!2PuBsb!tuY!<1GbQ7q*8P zInJD9eKPsufy&Hi!@0%ONLQZgOU7~70;l>5IRc)aV`)g%iwal{MUx+n-y#mpX{X~; zU>ZAe8w?h22sQHaT9LkFCHvgy{xzXG3!))QX^-`voiKBH&Yad)0w}&2i}9Q}tV(XN z&c!J&2Imd>VG)%$g+AmYz8_N`Z?Rzw)eT^#mR;C(=luX8asZ@MY+ok2<7>UTZ$~vf z>yA4et?X88Z9h!^3<|;N5f;O)`@%NiM@7TB2AVT& zpqHNXAYS)17;{E^ z#}VH_{e@~bjjA94S)WlvzB8H~aJV3T_j8w}m#DNt8LUs=<3=6{ zVU*}QKJ2$xsvKjaYPv-}77RVL?+HE=KRYV!J=()y@3l)n-CGg(`9S9LB>HO@0olhD zAx_HGKtXR15|`L}9TM1hoI9gr*1NPaslFV~#GVxeif0WhGeedV>sC)Iy}>2#@=ZtY z(x?Ji7ATBp2BtSw&g_o3zNfxWGTN(AeZ zRRyO?pgBI{^xvvRrau%-I+*W>5xTsQ&L2cgXhEz#+eMrT6=pqrXXRzBo`zHom=XV= zA*o@u&b)5aL(y-@gIE$kD-uvb(yULGbh)g5 z$HQpyu&u3}=b)wE$gMWYLEYC#wFo%*yfqdSFvgJh%p&G<%j%g!O5L#)!!A?zT zjqbIxs5a3gZ%OV;fBiRm{@IuzE-PEhb(dbrc-M%*nQVY<CjLPyzKsb$uvt#}jiTGPoo3JBDpEUI@F~S=Xi0*Cc^e}3B4C00<*#WP~5sY-j0IiV8!nH080NY=O z7ec|Y%1Pc<}Ft66P3)#uLkkO)W za1jh8N9Or^-~MgR0$7esMQ}1GcZv@{=MBykYJW!;M$a?e2Iqzh_$1bN_{HA|CyAhJ z{`^yxjU6Z|3g~nJGDbvNGDb^=y~pPrs4^U=q%=$$)DvT-bvfJQSl^p4QN6QEQ{BSl+GL$S7Gkm`f{%eF7LE#An2{E$ORLn}rJxvP;AedomlLqgcuVPp zo-k@}6xOJ`z`2tydKmI5>{hm0Z+$>`npAecx{b_>V=syVbU2(~wS&xp^5%y0S|z0@ z+c!6uWA$yXF>4XKO}d@^_&h1~QHTQKneWtrZMk|l6^AaO6ahF=alG+S@msEJpNV0- zH$5}pp3dI`F}V>c4Xc&{XSga2u(^e(1QI()I+TYOqRc^&GR;1Z7iOcJj-Te{{M}c( z{PzzJMh$gV9Gw5q{FXF19Msc+-BCy_uD3Z~7OQy(+j_usi_zi##UaV7Qo>CucsRX; zuqwF~AYUsmb*K}BDQ!L@OR@SlIA_i>YsqYjLIl1GqBBTHGY&}uWe}MyF-Zvg5SAjv zcsNltq|vj|ASBFhb#Io{eO(o~9@bhf0%C``me^mKvV#CC84JkWF3{R{5dq2jKo<^- zI+L<4pqqi8pWlz456Z7fd@2Y%yXzKWHS*%{Lw6WA9&b#sHDFiKoPw3?4dpmstO`S$ z-)i^MeUDcnvr5DpQGyK0fS3|2>8lW--Pq~8iD-yrQ197@()7!#;o>>%4>IWf`xLnD z7_ZX`Hwn_+!knky^?3<5W&L(xv64})#ny6PHT@C}Jh9%v-rEvm4zUzcW4kX!UNlXI zzVmzPLy8o>^t?-q+2p`SL6T?rd*5u82_1qLF9_^GH-d}@PZO975riJyAy=8+}K zbm2F%zOgwkmOT}Y#0oZQC40Ck~>Pz|%N&bNv4wTc9UE{4$nvk1G z`F!VfQL=AWu(XnT6@TVxK-rn6zaZp(ZLMe3%P`6ux10J$Cgz)FDT8bGu)obKCx~QR zrkyK>U@j|1az4~#{$%h9hiDsZ8-NJM}3?;GqPX^19}8c;h4g&E};K zhz+8%KdCX!znh7J0m9rLN4J%xl?Isv8E&$l*NyDAIJE-Mgh70L$v+1txeWPd`z4em zi*HDGhI_^~Kzz=$@r=J%`1DfG_5uIyPcny03f&z_)2hT5Un0jm?Nc#odavBc|LqD7&TTfu{EKi_o z&w+Cso@;m#w-S=*R4Hr6h8gd1cXb`WtJiM^k=v^00US+^_vzI-MYk;!NQu6-n`)Bj zDF@!=WZm8>=6^nZ?9(BS)^epbzc-&;{+QoYce0bOgkBP%#~7pA{NGbLQam=l?(H=T z)vuTA*&upYIn%CH73HsI35gM2Je(_H&aMclVA4l(PSssN&Fhhx)1_WpStPbD4R~~U zfvozR559@Fk!+K8`fyc*gi(l=G8KC6^n>!1%>&O?y9JTB07jSAwARqvC)9?E6%8a3 zR;>YVvrOvb{x=l&X5`#n<1^zbGE5pT?U{cqmmb*o0NTuXC)4DBDv1bBNP|?*8aqzL zo0H~5DKWop^^ON|$SKB*HK8)bJ{h?cQfr2Ho`MrM@m~*iXs%b|lJFR> z@??s>4jR2hS@P+^!(`D25X-GVq|QfGYT#Apw0O^k@^uWO$VhzJ&&0acsBEo}K(r+4 z@FZ*J7Uzn0IoceLg0!Lq+@h_oFPNkViE_30H6GgWMZs=-!eZ~dQz-v_M_bTp;D&Us z;I-jXbyj0_p6v$hvi=i`k!>6+CX zsaB~5{L9$n7G7czIIgC_5`0CgU@iamV731CQ2*KRW9V#1cR}~SJR0BvI+z-K)qK*` z#!-f8oA6UDk)q{p_u@@if|yk=lu5OnKKaFSWS3(>$3krl-RsO+Bt15|OCUXzDxOH? zP2j+_DXlN8zAV|$aKKWrPKwXgNwKPY|Dp1uIY*qq`?5q!D!?@*b+i>n zEKbPLaz>N3kj<>d>p{TFq|R@s{l<7&JtP!_2smZLI;3^y9J3PKQ~sEAqI(u?=La(TJ9yw7V+gJ_lT1EGt=D>_FR4Jh|lG)#n- z@>_~*kmKr@@r(uY|0bg-3}}7~>OyPPaQ7Sk=Jg8|k#fjPYh7#AE1-b7GHnVb16)F^ zdy`$P_m4CmOh-RfIh3|r^Q~rgwXhoh_f_qsT4(und_OR_=gQkCWOBZq8#*DlL3&=Y zExBU7%Ztn6!#lH_M^0496y4(%tD zy>9(2F(|-gT0UnK+Z|WE7^6y5I6B-FCYt7ZVn?Fl(hK*Z_36Sf)L8v&74ExhxUG%X zy*z6Y^e3dDa{c~voj2d#_IiH{ZE1b5vO@5*6l7RZ6LXYFV;;^Y3lTZ{LP;)n>W2|| z`Q05?-3tcLzord=Ti#SRlri0tKQqXm-2zirWuHMU8V}LRv*}?ybe%}7Ten6qB>eX6 zUGDCiAdXA`Zv7`#0a7dqGnPQB1L1Vg3IGXVWw^KkI(vxF+AyT|eOiOe?L=rci8Kxg z*)IdF$Zda7U%kQAa%)n}B6SN0-hK^ts=5C%VN5f+@h=k?eQ-n8qK51ck-|Vh;Qn(} zhtsFfCI9)RJQN8a33*8t7G^T`$9Eel^u>)A|M`HssLZ|5lP!J;t9b+J$I!mw)MO<5 z;uRjw`Bdo%^fa|7eGq(td9EAl9QeWL7P%V%#FXhP0f{koCvyiSh&jjLud5Ax0OfWhG zha|Tmxh@0|<1kl`RKcI4>t|w_wX{S+-w&XFib|V10c1*&8J8|y3e;mlDD+ARdiMy+ zpbrK6kylosHlwSFRG3f~wC2Z1tB{`&DSc*_%hN>dkQC%a0OG(iM@L?-4RHbyO9yZ7 zAXvtDHy+^utVLL!9mx-o$AXDw%tWnE9?7OfdN;{|qs&}bIn+lvQ)6A&-u3Jrf9hDD z#7ab?N9`kB{`te17YOiM%*_QMJKRWtFv|MZr>yt)jZYMYq@hgPc*sM5WAh*d=OTT7 zN&TiB{=O?CJ?2xc_=AP+(pUoQ&VH5z*L5{re6J3Keg988a+HbJsUVJGaKE;aUhms|NUf-6@ zqL7-JH#OQ@2I0lo2X7J4uv=5TRIAF3hm&ER+1u<5!sE%$ zM}_oeQY9cdFqA15U-J7)CPjf#k3_B`{dflDgkVG!%~Gv;2u}a~n)rNBA#OiT&Ad0< z-BqtAEA28ME3pm_kl0Xl71?k+i06&JZ~B3>1d?bp(J(r0vSq=vBJ-Ph%IFZ-P&u+2 zStd&YAZ>AOJ=JKcVRRqQC==q(R0%?NDBE5j;xhc{JL#_%ui;D27^2&CwCVi)N;W!2 z6hJt_;3$M9ef{RmTIdsE^(RHK3>kz=H{Iwz>$8xZpXkdNdRm?+fIg(o>lvFi;Td}T z^#TNwq-+}^@*Zf5hb)mP6CO$dnkg`nU_2e-nY98)EQs7_;Wr@)|6>EpL?HsOjFc*|yTE3% zSeDF;*?>JG4GMp0%SAYE;02#UMWl_K7ENO+6bPYgq0uEuRq{|yz~6_GTj8~c{1}ZA z(iChu2Vt5>u@ej_h?szRV|ok96v{9`a9S%U=t#8u>rd9>O^^QfrVpeK8A4xZq821T zs6vVdc`IPNC^iEp&|5|J??6*y5eku8l#)XiNtE!`fRNC(vjjj^ zDUTpHhE^IpbQ)AnLJzl;9-PT@NhztrXl=%}71g{!1FCdqkOi(cYE4)Uljl?!-Vpa`W#3Zcv#g!uDY{MYkOk0d z8a6{_CP-@CD8Dkp?f>2NBnV+|gN;cwx2Z)?l_?s05nxDrp84RvFdFtL* z1-EGNZR7qbt=Yw#d))q#2Tck@E{)GS|XH}RMF0m}TxRk>0Gx(eoIJ6Sq!F-b;?hlCSh@{%> z1`T{^c4jIEbUX!i@e&Pil{Gc&VD6uu4|~=u6-Af{QikPwi|$Fa=>yq^S)r$;qW=K! z>^;XDi_>OE4xWtG>lNRG<|5G-MLlq4fl!4q&NrHb2<}4QBUYjj%yPpJc$XygJ^*%w z)1T0^JTUqUOJ3ZyJuKoKw3^%CDd=6K!6tOA_5qEgJ}0()Dv;^HCxID-l74Rb3(4a$p_w+)%avu7cmTzg)fP zI&g!|#R9Y?Ua>VP0P~6j#jBPHvGSFDOF`%vCYG@Hc#bX5hx39E$6^C&80FzRTF`!z zZpN{mRVZ_GfM1kJA=I4!yxiB6TaMP~1E9dJ&tX=X$Vd@@Bu}F{`7PQexPvqz^;KB7 zREpM|B%ShT7y=Y~g$n$_E!cfDVJX6~qaJCr6SXiRlOmnN9uv;Uwo|GlYR+M%JRlQwk!NRDDAQqq) zrJIBz9OKT11MudO%|6;OXMh;CqOA@+ng9p4iKco1G!sToUv@epSXQ8^mthOiBP8~z zs39SmRH62_knO^@(f-z3=s2@z^+0+1EBX?3woAf01OjOho(nY~%Im5sYqWXrJC!n! zAF^QKn=oZ%6rlW1l>m!Gczh7>Pjdx_1x`KA5GQF zv^-E+b%sAnkpmcij;h5pAk)$MBpT&>nH5?}N}<;ggh%#4sV`H9x(%idZmHx?4mkIL zA>ZvlT6zTC$N3D?EZ?|wAwR7r>JUR(7pbB?KKAac0!(#fTM5(^X((q?P*c$@0k952 z7N+>vw`{3W9kv2~unUwc(TMEm|JH;aWbd(Gyq19a_1$TyabM?VO6H=PX3-({nkIgm z+fpRWCnw+%8fhGhhIT@tfE;gr#;=Gdtz>Shbr=bgGj45ajQ>d z!uEULq){J%4nqED4L=$+D&sd^omkA1nU*=YU#PjjYP(BQu5jw969|yXD8;GtB%I?H z4W&k!W)uyxA;mfJvpE6OQ=p#AlkED)Gu$FSxA57(h%AQje-(VIp*jMT5aof5>DC=AKQN40c5sUDN7`)2UO4 z`9@8s5fs#1ShQ51QEX?T&$6p~ldX4nL;NwrvLpe>=kzo`_FMv~vmm@Tn%bD0AnR}f z*8`8J2pRWsG$?(C)Ly|ONT_!5)K6v2(Do(mtMY;zBhFJdutYDKR_C)tslcw-Tki0u z4MZNxrTPHskM4iX4v-k|2K9lv;_ENiI7I@!HXQ-nNHDxoz7#6Ro~C8Tj0pB^WrxCrY8z>em~Mg zl++@q5&{6(1Zd*SGEo8rz1N|MN)-K9P&0hTn#qwh<0zR^uPqs5RAOQ}Q$IcNwy<>I z?q0V0`@1Vymn%cfPi~Q^;#|xlm4Mn4vT&XNqAnv=vC2~2%SU8Msc;I3(Tdi^j0EKGZeUXy52+#?Vgjh>|_MHkx_8L>g9cL`9?^>}Su zk(@1CMtLVcw_Cy?BT6hc`^es|s-lS+HIOpGL__KF1Nr40)I`$Fo^S=AXJLc)=rLeN zekXYv#oXi362xRPzt6V%UcI7mGUvLO5*RlKhbExG%1yIh89dROgtdqzci=3-Zr!>i zDTLhfFfjTJjx_G9fCvT{SLdBTq3_QRhdC0Z{)aOwzdDn`0_&l|l<(gVy!dh1h8ofG z1C3HhtHD3+01j~!k~O*`5NuG4qYV+6a}Fk zfEy@q;<7-kcL2x9?@P>>=vB9ya>V1G4KljgWN{THdT}mf$VYu}Zlar%TydueHPVW7 zXTf>fB38`DT~fXR$cH?tev=9#{Yo)f2lZn&$Gq!G_o0lk+!yCs!l%3ex6&lZ+anMn zBhob10K-#@Qna)XUE=#99f?SNv0k522Cbtt-&#Fm_tc%yE%hxmE`?Nxh$J}4(0Pf* zo+Q34Ph1%Yoj}AxU@=JRseto>qzKX=D&#xHd#=iEf>OYJjPn|Swd9(fzrh$HzSJsh z0R52mMi%^G@JVS*1gOkC6B%B1-LIlwuqBRzJkHWji z;NApKZA9KrfzlR-xeL9fz%xm$wiq0_F{t!afNRp5S9{j|juoXfN#X}^ndEW+2DB{m zqInQr;20Sb=^8L9_C7^<#z9Inhu?^D|N8;B2L%X~E%hcy(l}pP!1EG#{(ZWs!Vd@- zZjs6jbEy)}>}%mba`TVTm1#(_zd?7d<1h}aYZI&@R;m?ttubAeGpR8pP_q!K+1Xh1$>lKH#c_POANk?^Fo+i z+#ZvIIv%>P&jzof=_Vq?-YD#Uy8f|mZ*r7NjY`*V<2_YoSpW2&C;i`q<+mO2mfER^ zx}BxshDFN12FudSp16c+K0rGmPLf_^7!X#-vE0qG`>#_Oir`<6UCTW7MH8#*bLN!` zqIKvr_!VnX6CjPc(W(* z&^Iafw?Tt~v2Ma;LosVR=qlLoE}5r;+v5Cx$!Ua?-xI`3-Ifk!9RKjgr(+wTRB;yp zZV9Q5RS74zP@o0oy+B94=)bEM*l4)q1g#;&JmwPrmcLVrup)G% z+-v3(PERI!x}N!X-M>zT57AIoV#%{dYeSRjf8vIhBczG8H=lGuAtaqIGfyy-9FSUL z`x5#0Dje7livJ?jYR3bN{DRl$X{HKaya$Ta8+!FYQQ3e&oo~w}S!M7ZJ>Hjyi@0sa;OB>{Q5HU-r@2^V7OU17RIwOb-8qqm z_BEew0n!zHW2C=dHSFOD9rCvF6Chv+j}1>tQT6DrhDHvZYe+wn+(^WRYcun2Jv9xr zi!-m2_#V-AeYIh3CmMBIS<0>$+<1hWn^+ZV`yZ|Sd_b4Hu<+>rb(1!n#7(xC&3 z*QNPn21M*7bhH1{?|fsy6<*3bol09-CWRiH`MFJ4kS3;rmITlC=PbCHWAu>DpYESJ zV7s^a`OO+U#M|eGThdQtE+ibA7xM++Y$-t1>hGsZnk6rGb$7eY4*1vlYffHj*e-mL z8t%rX+`v)VS}G8_D2r{ypAn0>D(01Ddyb^Y&G-nwe=E%QZQn1=vp=Mr{rGx8FkHwa z(f70#Dgm&m|92k+ochg*0ntd(x`5sZPgKC3{QiE+6p(UsGV2n5^mqOTX9@$<)-Kz0 zvCPb`E0S=4&=;w_rCn*PZ44zn@i-Ju@qYLE;jTACIBQDCI3%({L}r5ycf@0EykCXT z5~Kxf=aSX0y07v$n<-cGLh^rRr>I-auF?mkTJ090i!OvN+JDHE=!q1M8_|z0{qpZ4 zFx@^U|He?ADlMY+f>f&mx@qPF#2X?E;@gObpii$uyRp6$Pbcw+XG-R7D}C|4>M63d zTjzY-ka8-;;Y@n#W_K=*#*Bdk3%4Dx%0@5z4#8PEg8 z(>%9`B)k57a* z++QKxoGT1|19uj&tNq%5OQWJ+joRLV6S2JlS>Zd>h*16FAW44#+^vF(n_2xB(&`pC zPSZSlUK|S0LR=vgX=qf3gaGMr*^{TDFEPNXTmPh?IQdHn2|y(2pWsMAzog~jCX(a; zd?wPCFq=^G63X(^@SVy(L!EZWMiD3@cj8_G?xx$Ps33-Pc6~nH_DmguLDJD5bDxwu zAkPiaoLx0fbOM~fT1$Y6B~b;V^Jc33)FA|^$OST%E6o@$uZE8@$zmv=gN^{ukmew= zSPHmtVW-2q+5f6m4U1pN1*>UVjt)$c;Db>AKkZ%nJJe|#pCKg`MNV66F)AvRbRef% z)(piOvr$B8C7qB%R4cR{s3p^2Dd#EagiP6vY_wwEC|RK`<+vKBQ7MPaes13VAKo9{ z>*~6?a+sO#^Zh>ebKjp+D)mMyf*^(@?+pC{l!iP90#Zx@+l2}hRabS1|GP|^rlyJ* z6s zfNIhEx2KJg1zLF?@ew6Ah2>Z+&^-e&YDUptzA zoSE-k8-$0G6dl-2ygHz87wF>+AgM=oMC>V*nKM%%J}Jo-_B~k#J(3`@8x3*YGNEM8 z0@~{Q5?dw{s3?3Jyo+F1x{dMe3>0wOb->DwZau2QgHE1G9v2E>9u;M{JQ&rSmST!L<;64Fwka+|4gZdbBt zumPq{Z?W4?_+}hA)at+ES5CtZi0B3GP=`&pw6zYigfwibF79uOO~$fK^!<@9w1mqJ z3D%<79=Jv4HR0aB#`n={ z!M(3jf!M_RuciT~4Y)ry*X7@!3OrL>$q*VM>g%uv{`F4Vs_7-j<7JKJ$FuWyKFi|G z4bX}f4MUq<5y6zL7x}6%DCz)A*Nww$gJocL+M>j4;TEI-n9*PZAd5jbHnDxk9idy7 zhNVxi$NdfIBGw&}PymTC{!iCVS_S}roKeZWhxCz3dHhiLjZldf;zux9k1(w8;AsF3 z0MSpK>UzNXf_Qh^to0uOiorSgHG*6wTcJ~?;3D1d@tk5yEOO;tJ^RF`WUXwds;Jn8 zy)sUu@CjPx-$J=Fa0Kh_0Bp$hPLMtWBcuWX zi~t4}_;PQN(>P37$;Q<;Zn&=Nv?(8BDG6=S%`kV?UYbF(%{VG4To2iR&`Y$Z;l|iT zR;WSI1IgnJwKRjIiZp&<;Xn;==V1-Flkw@J=maM{H@sa86tOlGh8&>WSWQ7EaBso4 zcvDkCD1lNhMUV=Ebh)$>5f(;UilVzRKx&*)UlO7f9kXHWS|&m}+JUIZQIaAZ9{>j? zrr3|CeCfBCDtBKe7*bKI5SWdz;Q5>j=f!N%a#$>Bm!mApWkByh zR4bgL`1Jf3dQ(lj(wDqYvntHzVJ$zef=eL?6&1{w1=jV0Jyy)1#od@MHU=g(mO3i7z!`;sWV1<@e1{D;O793aZiYBeoufv3cJRfa#tQPseRpvYVQ1jDd(nwx?mCuZr{?vFO znoErpe9n!?MROEq%7`P6ZQ68L`;^n?mOlbQHmdD)yV>08ai4s4)V{y<|E%7DSpx%= zIW#vjs0?C4CA96iM4;zegbUY~?WTV27%NrG29hWB$H~w94%*3sAa`K9Oi2vveN0*q z+{!0qIcZ*S#`5G(bSlhFWj|GT%uJD0J<#kL@Wy_Qv^-BLQ*Enr^>n!}uh=5ze8^WQ zbyM%6j2lzT~fAFAXlKV zIAx_bA|8Ps6LZY~>WMZd2$@bA)rK6say(W;$3UW^jXJPftD??{-|TAJj~liWs9_Up ziiik%Y&J?*TxWD>pvw!76zUndK~pPhke`QL9rH1^38x|8n>dSAui2%pr|)iG=D_;3 zSyH@%vFdo~dVAJqSfH2g8wvGWt)IIdhb}+HCLmAuHhv}9EUe!l-SVrS_rDJ72*5Gb zBdN{9wfYZwCrRpI-&8HxJF|Bvwwcf9VIo;X zVu5pLAQB|v%1!*^s?+dF*;!6?y*Jf+Vc-IXv;ZjIcy@9wVvNZ8Pj>!O-MG+lCq@3l}vxwJdzI>e+%|Y}SuOO&3ufDcDwLECJv53ic=j6E0WF z-63Mdson-tMmxoiwp&oAqGUd@R4Gx?&&*%a$6fS$^XcjH9@bA_9F?#bt8KntGzcfh`9v2`wf{kX^ z*%K#NCiJ#uh`6}-zN-+xdP3!_X=*3VKHT=e_f<;ujwcGp7$DsRS0B;xWJVptNp+wz z0R8ExO>TW%9u~f#y(KVZMYcH0<}bI|j#+{s^}a)I(CA*kfkuerSX_N4m}9^AekXLt zT8S~&it~86HT%*7DWK3(bn@9F$ppKC>z1Q7X+8Nuu=y`QCb}lD^ek+qe+7eMjpg`mo z(NN&}ZqSZ~KfDnRSl!09)}`XT z3KAQr-Wzsgcp6s*l+P{&Vn<=^b_kWKXwB=L$Kei&wv|rYpR%fB)gMXke=s@78CmZC z?tzY@tD0JGho0Ak=6}kAi4HN=uA7PsDJvg! z){l~;&yh>5CXNu<=PL7WNVlB`Sihjmvwr`QpKE%8-(>6*{P|J*9%VgIiE|+ZH%XXI zNuV*WzL=lHtA>Oq&f>!(g--Z{QbWZ4Y9wre>EYW1K+oW{8%yO-*sFIm&` z>ru+*y62HSv0HbBwv5->NyEu6BATpqPES3dX|zXU$%fQ-ud4JCCS$wtZy`jXYU}S^ z4YQFJrp~;U?KJwe+qc?LXC_=^Zqb<$%T(U`vj6pP@A4lWPtSU`(<+zc`+^?f#z^$% z{pm9i5(6$XGpv z`|_Pw=qG`HEfz}h9lMdvqtt86c`>dkNTw(9h`<2egG|Ba&Q$;{FdWJZP}K1Q;K zPjV9uKcw{Gu_?(2Sy4$3Pl5H}+&E5QC|PZCW@r~>aS#`~u;kG|n1LxaikRl4SWn~w%CjiWNZ|JiVF;os-{*eqoR7(oD|&bsk)*5FSv zOHIK^L$Pq~OfP1>&cO5jf#bpA8v3s-dK%6m)2asZZ$w%GD`6_#1-?)eFyqeH-?VK& m_|E$8m;e9I|NYe%ly)SVWeMt;LHO@;7#tf1>+9>bM*Rcqte%(v From a67eb194c15fb441139e716694d76807ec3ea4a0 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 12 Dec 2021 11:26:45 +0100 Subject: [PATCH 0553/1681] doc: emphasize that the Configuration object is supposed to be used as a singleton --- src/igraph/__init__.py | 4 ++++ src/igraph/configuration.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 7f616ef1f..e4432f571 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -5289,4 +5289,8 @@ def write(graph, filename, *args, **kwds): config = init_configuration() +"""The main configuration object of igraph. Use this object to modify igraph's +behaviour, typically when used in interactive mode. +""" + del construct_graph_from_formula diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index e81a1dfa4..8e28835c3 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -88,6 +88,14 @@ def get_platform_image_viewer(): class Configuration: """Class representing igraph configuration details. + Note that there is one primary instance of this class, which is used by + igraph itself to retrieve configuration parameters when needed. You can + access this instance with the L{instance()} method. You I{may} construct + other instances by invoking the constructor directly, but these instances + will I{not} affect igraph's behaviour. If you are interested in configuring + igraph, use L{igraph.config} to get hold of the singleton instance and then + modify it. + General ideas ============= From 387b8c299b20a9532b3130760e840aed763dabc0 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sun, 12 Dec 2021 23:36:00 +1100 Subject: [PATCH 0554/1681] Remove ax.set_aspect(1) from all scripts --- .../assets/bipartite_matching.py | 2 -- .../bipartite_matching/bipartite_matching.rst | 1 - .../assets/bipartite_matching_maxflow.py | 1 - .../bipartite_matching_maxflow.rst | 1 - .../erdos_renyi/assets/erdos_renyi.py | 4 ---- .../tutorials/erdos_renyi/erdos_renyi.rst | 4 ---- .../tutorials/quickstart/assets/quickstart.py | 7 +++---- .../quickstart/figures/social_network.png | Bin 28909 -> 65370 bytes .../tutorials/quickstart/quickstart.rst | 7 +++---- .../ring_animation/assets/ring_animation.py | 1 - 10 files changed, 6 insertions(+), 22 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py index c92c64800..d675d5dc7 100644 --- a/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py @@ -29,8 +29,6 @@ vertex_color="lightblue", edge_width=[2.5 if e.target == matching.match_of(e.source) else 1.0 for e in g.es] ) -ax.set_aspect(1) - plt.show() # Matching is: diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index d2bd96527..9eb753699 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -52,7 +52,6 @@ And finally display the bipartite graph with matchings highlighted. vertex_color="lightblue", edge_width=[2.5 if e.target == matching.match_of(e.source) else 1.0 for e in g.es] ) - ax.set_aspect(1) plt.show() The received output is diff --git a/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py index 6b7624dfb..c49327db9 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py +++ b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py @@ -42,5 +42,4 @@ vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] ) -ax.set_aspect(1) plt.show() diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index a53a1b42a..78fd976fc 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -67,7 +67,6 @@ And finally, display the original flow graph nicely with the matchings added vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] ) - ax.set_aspect(1) plt.show() The received output is: diff --git a/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py b/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py index b546d0dfd..1b8127965 100644 --- a/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py +++ b/doc/source/tutorials/erdos_renyi/assets/erdos_renyi.py @@ -26,14 +26,12 @@ layout="circle", vertex_color="lightblue" ) -axs[0].set_aspect(1) ig.plot( g2, target=axs[1], layout="circle", vertex_color="lightblue" ) -axs[1].set_aspect(1) plt.show() fig, axs = plt.subplots(1, 2, figsize=(10, 5)) @@ -44,7 +42,6 @@ vertex_color="lightblue", vertex_size=0.15 ) -axs[0].set_aspect(1) ig.plot( g4, target=axs[1], @@ -52,7 +49,6 @@ vertex_color="lightblue", vertex_size=0.15 ) -axs[1].set_aspect(1) plt.show() # IGRAPH U--- 15 18 -- diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index d4a451e5f..59e2ad0f1 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -43,14 +43,12 @@ We generate two graphs of each, so we can confirm that our graph generator is tr layout="circle", vertex_color="lightblue" ) - axs[0].set_aspect(1) ig.plot( g2, target=axs[1], layout="circle", vertex_color="lightblue" ) - axs[1].set_aspect(1) plt.show() fig, axs = plt.subplots(1, 2, figsize=(10, 5)) @@ -61,7 +59,6 @@ We generate two graphs of each, so we can confirm that our graph generator is tr vertex_color="lightblue", vertex_size=0.15 ) - axs[0].set_aspect(1) ig.plot( g4, target=axs[1], @@ -69,7 +66,6 @@ We generate two graphs of each, so we can confirm that our graph generator is tr vertex_color="lightblue", vertex_size=0.15 ) - axs[1].set_aspect(1) plt.show() The received output is: diff --git a/doc/source/tutorials/quickstart/assets/quickstart.py b/doc/source/tutorials/quickstart/assets/quickstart.py index 927e9046f..52edfab77 100644 --- a/doc/source/tutorials/quickstart/assets/quickstart.py +++ b/doc/source/tutorials/quickstart/assets/quickstart.py @@ -24,15 +24,14 @@ target=ax, layout="circle", # print nodes in a circular layout vertex_size=0.1, - vertex_color=["lightblue" if gender == "M" else "pink" for gender in g.vs["gender"]], - vertex_frame_width=2.0, + vertex_color=["steelblue" if gender == "M" else "salmon" for gender in g.vs["gender"]], + vertex_frame_width=4.0, vertex_frame_color="white", vertex_label=g.vs["name"], vertex_label_size=7.0, edge_width=[2 if married else 1 for married in g.es["married"]], - edge_color=["#F00" if married else "#000" for married in g.es["married"]], + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]] ) -ax.set_aspect(1) plt.show() diff --git a/doc/source/tutorials/quickstart/figures/social_network.png b/doc/source/tutorials/quickstart/figures/social_network.png index c9e901addd02eda2199681acfa788d27008c97ea..f528c450abd21668dbea3e4a1e7d2eef2e1e39db 100644 GIT binary patch literal 65370 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%0Ao@8SF>iJ!=AXDhWCGOBDQO)#uM=_9nE0PW_j? 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
  • pHIR53IAi*J;V6W35P~R} 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 diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 74ba5c7e9..7237998aa 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -43,15 +43,14 @@ Check out our in-depth tutorial TODO LINK and our gallery TODO LINK for more. target=ax, layout="circle", # print nodes in a circular layout vertex_size=0.1, - vertex_color=["lightblue" if gender == "M" else "pink" for gender in g.vs["gender"]], - vertex_frame_width=2.0, + vertex_color=["steelblue" if gender == "M" else "salmon" for gender in g.vs["gender"]], + vertex_frame_width=4.0, vertex_frame_color="white", vertex_label=g.vs["name"], vertex_label_size=7.0, edge_width=[2 if married else 1 for married in g.es["married"]], - edge_color=["#F00" if married else "#000" for married in g.es["married"]], + edge_color=["#7142cf" if married else "#AAA" for married in g.es["married"]] ) - ax.set_aspect(1) plt.show() diff --git a/doc/source/tutorials/ring_animation/assets/ring_animation.py b/doc/source/tutorials/ring_animation/assets/ring_animation.py index 02e028ec5..55d080643 100644 --- a/doc/source/tutorials/ring_animation/assets/ring_animation.py +++ b/doc/source/tutorials/ring_animation/assets/ring_animation.py @@ -9,7 +9,6 @@ # Create canvas fig, ax = plt.subplots() -ax.set_aspect(1) # Prepare interactive backend for autoupdate plt.ion() From bd82befc11adf2f67ff4f64e34cb1236ba00a4db Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sun, 12 Dec 2021 23:45:38 +1100 Subject: [PATCH 0555/1681] Fix figure captions --- .../bipartite_matching_maxflow.rst | 2 +- doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 16 +++++++--------- doc/source/tutorials/quickstart/quickstart.rst | 1 + 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index 78fd976fc..c8aed55f5 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -8,7 +8,7 @@ Maximum Bipartite Matching by Maximum Flow This example presents how to visualise bipartite matching using maximum flow. -.. note:: :meth:`maximum_bipartite_matching` is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching<>`_. +.. note:: :meth:`maximum_bipartite_matching` is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching <>`_. .. TODO: add link to Maximum Bipartite Matching diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index 59e2ad0f1..e5c3132df 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -6,7 +6,7 @@ Erdős-Rényi Graph ================= -This example demonstrates how to generate `Erdős-Rényi Graphs`_. There are two variants of graphs: +This example demonstrates how to generate `Erdős-Rényi Graphs `_. There are two variants of graphs: - :meth:`Erdos_Renyi(n, m)` will pick a graph uniformly at random out of all graphs with ``n`` nodes and ``m`` edges. - :meth:`Erdos_Renyi(n, p)` will generate a graph where each edge between any two pair of nodes has an independent probability ``p`` of existing. @@ -78,18 +78,16 @@ The received output is: IGRAPH U--- 20 35 -- .. figure:: ./figures/erdos_renyi_p.png + :alt: The visual representation of a randomly generated Erdos Renyi graph + :align: center - :alt: The visual representation of a randomly generated Erdos Renyi graph - :align: center - - Erdos Renyi random graphs with probability ``p`` = 0.2 + Erdos Renyi random graphs with probability ``p`` = 0.2 .. figure:: ./figures/erdos_renyi_m.png + :alt: The second visual representation of a randomly generated Erdos Renyi graph + :align: center - :alt: The second visual representation of a randomly generated Erdos Renyi graph - :align: center - - Erdos Renyi random graphs with ``m`` = 35 edges + Erdos Renyi random graphs with ``m`` = 35 edges .. note:: diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 7237998aa..7b3256ce8 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -69,4 +69,5 @@ The output of the code is pictured below .. figure:: ./figures/social_network.png :alt: The visual representation of a small friendship group :align: center + The Output Graph From a1d1eb78c5d0888df3fa39ec82971f4e65e3c0f1 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sun, 12 Dec 2021 23:56:21 +1100 Subject: [PATCH 0556/1681] Fix gallery links --- doc/source/gallery.rst | 2 +- .../tutorials/bipartite_matching/bipartite_matching.rst | 4 ++-- .../bipartite_matching_maxflow.rst | 4 ++-- doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 4 ++-- doc/source/tutorials/maxflow/maxflow.rst | 4 ++-- doc/source/tutorials/quickstart/quickstart.rst | 6 +++--- doc/source/tutorials/shortest_paths/shortest_paths.rst | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index 517727fdc..69fff1990 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -1,6 +1,6 @@ .. include:: include/global.rst -.. gallery +.. _gallery: ======== Gallery diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index 9eb753699..b421189a8 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -1,6 +1,6 @@ -.. include:: include/global.rst +.. include:: ../../include/global.rst -.. tutorials-bipartite-matching +.. _tutorials-bipartite-matching: ========================== Maximum Bipartite Matching diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index c8aed55f5..11abe3029 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -1,6 +1,6 @@ -.. include:: include/global.rst +.. include:: ../../include/global.rst -.. tutorials-bipartite-matching-maxflow +.. _tutorials-bipartite-matching-maxflow: ========================================== Maximum Bipartite Matching by Maximum Flow diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index e5c3132df..f702da660 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -1,6 +1,6 @@ -.. include:: include/global.rst +.. include:: ../../include/global.rst -.. tutorials-random +.. _tutorials-random: ================= Erdős-Rényi Graph diff --git a/doc/source/tutorials/maxflow/maxflow.rst b/doc/source/tutorials/maxflow/maxflow.rst index 5a078f296..d7371b196 100644 --- a/doc/source/tutorials/maxflow/maxflow.rst +++ b/doc/source/tutorials/maxflow/maxflow.rst @@ -1,6 +1,6 @@ -.. include:: include/global.rst +.. include:: ../../include/global.rst -.. tutorials-maxflow +.. _tutorials-maxflow: ============ Maximum Flow diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 7b3256ce8..56b926b22 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -1,6 +1,6 @@ -.. include:: include/global.rst +.. include:: ../../include/global.rst -.. tutorials-quickstart +.. _tutorials-quickstart: =========== Quick Start @@ -69,5 +69,5 @@ The output of the code is pictured below .. figure:: ./figures/social_network.png :alt: The visual representation of a small friendship group :align: center - + The Output Graph diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index c7623fc0c..4f8f14700 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -1,6 +1,6 @@ -.. include:: include/global.rst +.. include:: ../../include/global.rst -.. tutorials-shortest-paths +.. _tutorials-shortest-paths: ============== Shortest Paths From 3109fe451aba0270367bbb29cd526b62d4011e85 Mon Sep 17 00:00:00 2001 From: h5jam Date: Sun, 12 Dec 2021 22:30:43 +0900 Subject: [PATCH 0557/1681] doc: fix topological_sort.rst --- .../tutorials/topological_sort/topological_sort.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/source/tutorials/topological_sort/topological_sort.rst b/doc/source/tutorials/topological_sort/topological_sort.rst index 09e92fc2e..bbe53eecf 100644 --- a/doc/source/tutorials/topological_sort/topological_sort.rst +++ b/doc/source/tutorials/topological_sort/topological_sort.rst @@ -6,7 +6,10 @@ Topological Sort ================ -To get a topological sort of directed acyclic graph(DAG), we can use :meth:`topological_sortng`. +This example demonstrates how to get a topological sorting list on a directed acyclic graph (DAG). +Topological sort of a directed graph is a linear ordering based on the precedence implied by the directed edges and it exists iff the graph doesn't have any directed cycle. + +We can use :meth:`topological_sortng` to get a topological ordering. .. code-block:: python @@ -25,7 +28,7 @@ To get a topological sort of directed acyclic graph(DAG), we can use :meth:`topo results = g.topological_sorting(mode='in') # results = [5, 3, 1, 4, 2, 0] print('Topological sort of graph g on 'in' mode:', *results) -There are two modes of :meth:`topological_sorting`. Default mode is 'out', it starts a topological sorting from the node with indegree 0. The other mode is 'in', it starts a topological sorting from the node that has maximum indegree. +There are two modes of :meth:topological_sorting. 'out' is the default mode which start from a node with indegree equal to 0. The other mode is 'in', and it similarly starts from a node with outdegree equal to 0. The output of the code above is: @@ -35,7 +38,7 @@ The output of the code above is: Topological sort of graph g on 'in' mode: 5 3 1 4 2 0 -For finding indegree of each node, we can use :meth:`indegree()`. +We can use :meth:`indegree()` to find the indegree of the node. .. code-block:: python @@ -64,4 +67,5 @@ For finding indegree of each node, we can use :meth:`indegree()`. The graph `g` -- Note that :meth:`topological_sorting` returns a list of vertice ID paths and we can set two modes. +- :meth:`topological_sorting` returns a list of node IDs. +- We can set two modes as a parameter. From 3fc0f22b5605da8eaf237d812cd9b2a7ef6a54cc Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Mon, 13 Dec 2021 00:36:41 +1100 Subject: [PATCH 0558/1681] Add links to external sites --- doc/source/gallery.rst | 1 + .../bipartite_matching/bipartite_matching.rst | 5 ++++- .../bipartite_matching_maxflow.rst | 11 +++++++---- doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 10 +++++++--- doc/source/tutorials/maxflow/maxflow.rst | 5 ++++- .../tutorials/ring_animation/ring_animation.rst | 9 ++++++--- .../tutorials/shortest_paths/shortest_paths.rst | 11 ++++++++--- 7 files changed, 37 insertions(+), 15 deletions(-) diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index 69fff1990..f3782f6ea 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -12,5 +12,6 @@ This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-bipartite-matching` - :ref:`tutorials-bipartite-matching-maxflow` - :ref:`tutorials-random` + - :ref:`tutorials-ring-animation` - :ref:`tutorials-maxflow` - :ref:`tutorials-shortest-paths` diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index b421189a8..eaf28f320 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -6,7 +6,10 @@ Maximum Bipartite Matching ========================== -This example demonstrates an efficient way to find and visualise a maximum biparite matching. First construct a bipartite graph +.. _maximum_bipartite_matching: https://igraph.org/python/doc/api/igraph.Graph.html#maximum_bipartite_matching +.. |maximum_bipartite_matching| replace:: :meth:`maximum_bipartite_matching` + +This example demonstrates an efficient way to find and visualise a maximum biparite matching using |maximum_bipartite_matching|_. First construct a bipartite graph .. code-block:: python diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index 11abe3029..83ed53426 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -6,11 +6,14 @@ Maximum Bipartite Matching by Maximum Flow ========================================== -This example presents how to visualise bipartite matching using maximum flow. +.. _maximum_bipartite_matching: https://igraph.org/python/doc/api/igraph.Graph.html#maximum_bipartite_matching +.. |maximum_bipartite_matching| replace:: :meth:`maximum_bipartite_matching` +.. _maxflow: https://igraph.org/python/doc/api/igraph.Graph.html#maxflow +.. |maxflow| replace:: :meth:`maxflow` -.. note:: :meth:`maximum_bipartite_matching` is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching <>`_. +This example presents how to visualise bipartite matching using maximum flow (see |maxflow|_). -.. TODO: add link to Maximum Bipartite Matching +.. note:: |maximum_bipartite_matching|_ is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out :ref:`tutorials-bipartite-matching`. .. code-block:: python @@ -35,7 +38,7 @@ This example presents how to visualise bipartite matching using maximum flow. flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1.0 print("Size of maximum matching (maxflow) is:", flow.value) -Let's compare the output against :meth:`maximum_bipartite_matching` +Let's compare the output against |maximum_bipartite_matching|_ .. code-block:: python diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index f702da660..64b31c4e0 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -6,10 +6,14 @@ Erdős-Rényi Graph ================= -This example demonstrates how to generate `Erdős-Rényi Graphs `_. There are two variants of graphs: +.. _Erdos_Renyi: https://igraph.org/python/doc/api/igraph.Graph.html#Erdos_Renyi +.. |Erdos_Renyi| replace:: :meth:`Erdos_Renyi` -- :meth:`Erdos_Renyi(n, m)` will pick a graph uniformly at random out of all graphs with ``n`` nodes and ``m`` edges. -- :meth:`Erdos_Renyi(n, p)` will generate a graph where each edge between any two pair of nodes has an independent probability ``p`` of existing. + +This example demonstrates how to generate `Erdős-Rényi Graphs `_ using |Erdos_Renyi|_. There are two variants of graphs: + +- ``Erdos_Renyi(n, m)`` will pick a graph uniformly at random out of all graphs with ``n`` nodes and ``m`` edges. +- ``Erdos_Renyi(n, p)`` will generate a graph where each edge between any two pair of nodes has an independent probability ``p`` of existing. We generate two graphs of each, so we can confirm that our graph generator is truly random. diff --git a/doc/source/tutorials/maxflow/maxflow.rst b/doc/source/tutorials/maxflow/maxflow.rst index d7371b196..7e8b999e1 100644 --- a/doc/source/tutorials/maxflow/maxflow.rst +++ b/doc/source/tutorials/maxflow/maxflow.rst @@ -6,7 +6,10 @@ Maximum Flow ============ -This example shows how to construct a max flow on a directed graph with edge capacities. +.. _maxflow: https://igraph.org/python/doc/api/igraph.Graph.html#maxflow +.. |maxflow| replace:: :meth:`maxflow` + +This example shows how to construct a max flow on a directed graph with edge capacities using |maxflow|_. .. code-block:: python diff --git a/doc/source/tutorials/ring_animation/ring_animation.rst b/doc/source/tutorials/ring_animation/ring_animation.rst index 9add9a945..8fbe53cc5 100644 --- a/doc/source/tutorials/ring_animation/ring_animation.rst +++ b/doc/source/tutorials/ring_animation/ring_animation.rst @@ -1,12 +1,12 @@ .. include:: ../../include/global.rst -.. tutorials-ring-animation +.. _tutorials-ring-animation: ==================== Ring Graph Animation ==================== -This example demonstrates how to use Matplotlib's `animation features `_ in order to animate a ring graph sequentially being revealed. +This example demonstrates how to use `Matplotlib's animation features `_ in order to animate a ring graph sequentially being revealed. .. code-block:: python @@ -53,6 +53,9 @@ The received output is: :caption: Sequentially animated ring graph. +.. _induced_subgraph: https://igraph.org/python/api/latest/igraph._igraph.GraphBase.html#induced_subgraph +.. |induced_subgraph| replace:: :meth:`induced_subgraph` + .. note:: - We use *igraph*'s :meth:`Graph.subgraph()` (a.k.a `Graph.induced_subgraph()``_) in order to obtain a section of the ring graph at a time for each frame. + We use *igraph*'s :meth:`Graph.subgraph()` (see |induced_subgraph|_) in order to obtain a section of the ring graph at a time for each frame. diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index 4f8f14700..28a75253f 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -6,9 +6,14 @@ Shortest Paths ============== +.. _get_shortest_paths: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#get_shortest_paths +.. |get_shortest_paths| replace:: :meth:`get_shortest_paths` +.. _get_all_shortest_paths: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#get_all_shortest_paths +.. |get_all_shortest_paths| replace:: :meth:`get_all_shortest_paths` + This example will demonstrate how to find the shortest distance between two vertices on a weighted and unweighted graph. -To find the shortest path or distance between two nodes, we can use :meth:`get_shortest_paths()`. If we're only interested in counting the unweighted distance, then +To find the shortest path or distance between two nodes, we can use |get_shortest_paths|_. If we're only interested in counting the unweighted distance, then we can do the following: .. code-block:: python @@ -67,8 +72,8 @@ The output of these these two shortest paths are: .. note:: - - :meth:`get_shortest_paths` returns a list of lists becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. - - If you're interested in finding *all* shortest paths, take a look at :meth:`get_all_shortest_paths`. + - |get_shortest_paths|_ returns a list of lists becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. + - If you're interested in finding *all* shortest paths, take a look at |get_all_shortest_paths|_. From 30b4c6955309b80e2bb0f8fa868a9dc9765546aa Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Mon, 13 Dec 2021 00:38:31 +1100 Subject: [PATCH 0559/1681] Make tutorial captions more consistent I.e. capitalise all captions --- doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 4 ++-- doc/source/tutorials/maxflow/maxflow.rst | 2 +- doc/source/tutorials/ring_animation/ring_animation.rst | 4 ++-- doc/source/tutorials/shortest_paths/shortest_paths.rst | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index 64b31c4e0..f980f7ee9 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -85,13 +85,13 @@ The received output is: :alt: The visual representation of a randomly generated Erdos Renyi graph :align: center - Erdos Renyi random graphs with probability ``p`` = 0.2 + Erdos Renyi Random Graphs With Probability ``p`` = 0.2 .. figure:: ./figures/erdos_renyi_m.png :alt: The second visual representation of a randomly generated Erdos Renyi graph :align: center - Erdos Renyi random graphs with ``m`` = 35 edges + Erdos Renyi Random Graphs With Number of Edges ``m`` = 35 .. note:: diff --git a/doc/source/tutorials/maxflow/maxflow.rst b/doc/source/tutorials/maxflow/maxflow.rst index 7e8b999e1..0e93ec577 100644 --- a/doc/source/tutorials/maxflow/maxflow.rst +++ b/doc/source/tutorials/maxflow/maxflow.rst @@ -41,4 +41,4 @@ The received output is: :alt: A visual representation of the flow graph :align: center - The flow graph + The Flow Graph diff --git a/doc/source/tutorials/ring_animation/ring_animation.rst b/doc/source/tutorials/ring_animation/ring_animation.rst index 8fbe53cc5..455913572 100644 --- a/doc/source/tutorials/ring_animation/ring_animation.rst +++ b/doc/source/tutorials/ring_animation/ring_animation.rst @@ -47,10 +47,10 @@ This example demonstrates how to use `Matplotlib's animation features Date: Mon, 13 Dec 2021 00:43:38 +1100 Subject: [PATCH 0560/1681] Fix shortest_path.rst figure aspect ratio --- .../tutorials/quickstart/quickstart.rst | 2 +- .../shortest_paths/figures/shortest_path.png | Bin 30009 -> 27685 bytes .../shortest_paths/shortest_paths.rst | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 56b926b22..384f68b87 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -13,7 +13,7 @@ For the eager folks out there, this intro will give you a quick overview of the - Save the plot as an image - Export and import a graph as a ``.gml`` file -Check out our in-depth tutorial TODO LINK and our gallery TODO LINK for more. +To find out more features that |igraph| has to offer, check out the :ref:`gallery`! .. code-block:: python diff --git a/doc/source/tutorials/shortest_paths/figures/shortest_path.png b/doc/source/tutorials/shortest_paths/figures/shortest_path.png index b48156a38a4b5aa190729a4cfef443bc940b9924..7c77c04e0cecda3ffc3d1b06626701efffed84b5 100644 GIT binary patch literal 27685 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 N?AO%Oc&ug}@GtrOV|f4o literal 30009 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 Date: Sun, 12 Dec 2021 22:46:05 +0900 Subject: [PATCH 0561/1681] doc: fix topological_sort.rst and codes --- .../assets/topological_sort.py | 11 +++++---- .../topological_sort/topological_sort.rst | 24 +++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/doc/source/tutorials/topological_sort/assets/topological_sort.py b/doc/source/tutorials/topological_sort/assets/topological_sort.py index 02b384b5e..eebc7de3a 100644 --- a/doc/source/tutorials/topological_sort/assets/topological_sort.py +++ b/doc/source/tutorials/topological_sort/assets/topological_sort.py @@ -1,17 +1,18 @@ import igraph as ig -# generate directed acyclic graph +# generate a directed acyclic graph (DAG) g = ig.Graph(edges=[(0, 1), (0, 2), (1, 3), (2, 4), (4, 3), (3, 5), (4, 5)], directed=True) assert g.is_dag -# topological sorting +# g.topological_sorting() returns a list of node IDs. +# If the given graph is not DAG, the error will occur. results = g.topological_sorting(mode='out') -print('Topological sorting result (out):', *results) +print('Topological sort of graph g (out):', *results) results = g.topological_sorting(mode='in') -print('Topological sorting result (in):', *results) +print('Topological sort of graph g (in):', *results) -# print indegree of each node +# g.vs[i].indegree() returns the indegree of the node (which is g.vs[i]). for i in range(g.vcount()): print('degree of {}: {}'.format(i, g.vs[i].indegree())) \ No newline at end of file diff --git a/doc/source/tutorials/topological_sort/topological_sort.rst b/doc/source/tutorials/topological_sort/topological_sort.rst index bbe53eecf..8c69edbd8 100644 --- a/doc/source/tutorials/topological_sort/topological_sort.rst +++ b/doc/source/tutorials/topological_sort/topological_sort.rst @@ -15,18 +15,18 @@ We can use :meth:`topological_sortng` to get a topological ordering. import igraph as ig - # generate directed acyclic graph(DAG) + # generate a directed acyclic graph (DAG) g = ig.Graph(edges=[(0, 1), (0, 2), (1, 3), (2, 4), (4, 3), (3, 5), (4, 5)], directed=True) assert g.is_dag - - # g.topological_sorting() returns a list of vertex ID paths. - # If the given graph is not DAG, error will be returned. - results = g.topological_sorting(mode='out') # results = [0, 1, 2, 4, 3, 5] - print('Topological sort of graph g on 'out' mode:', *results) - results = g.topological_sorting(mode='in') # results = [5, 3, 1, 4, 2, 0] - print('Topological sort of graph g on 'in' mode:', *results) + # g.topological_sorting() returns a list of node IDs + # If the given graph is not DAG, the error will occur. + results = g.topological_sorting(mode='out') + print('Topological sort of graph g (out):', *results) + + results = g.topological_sorting(mode='in') + print('Topological sort of graph g (in):', *results) There are two modes of :meth:topological_sorting. 'out' is the default mode which start from a node with indegree equal to 0. The other mode is 'in', and it similarly starts from a node with outdegree equal to 0. @@ -34,8 +34,8 @@ The output of the code above is: .. code-block:: - Topological sort of graph g on 'out' mode: 0 1 2 4 3 5 - Topological sort of graph g on 'in' mode: 5 3 1 4 2 0 + Topological sort of graph g (out): 0 1 2 4 3 5 + Topological sort of graph g (in): 5 3 1 4 2 0 We can use :meth:`indegree()` to find the indegree of the node. @@ -44,11 +44,11 @@ We can use :meth:`indegree()` to find the indegree of the node. import igraph as ig - # generate directed acyclic graph(DAG) + # generate directed acyclic graph (DAG) g = ig.Graph(edges=[(0, 1), (0, 2), (1, 3), (2, 4), (4, 3), (3, 5), (4, 5)], directed=True) - # g.vs[i].indegree() returns the indegree of each vertex(which is g.vs[i]). + # g.vs[i].indegree() returns the indegree of the node (which is g.vs[i]). for i in range(g.vcount()): print('degree of {}: {}'.format(i, g.vs[i].indegree())) From 3a17830763951f21c99bfa6c4c88bc4cd759de77 Mon Sep 17 00:00:00 2001 From: h5jam Date: Sun, 12 Dec 2021 23:14:54 +0900 Subject: [PATCH 0562/1681] chore: ignore .DS_Store --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 44657c996..5495ecd24 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ result_images/ .vscode/ vendor/build/ vendor/install/ +.DS_Store From 036c1eafd1f354a9cb25325845b5df65d3de4ce0 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 13 Dec 2021 08:00:29 +1100 Subject: [PATCH 0563/1681] fix: Edge labels --- src/igraph/drawing/baseclasses.py | 6 ++++-- src/igraph/drawing/cairo/graph.py | 2 +- src/igraph/drawing/matplotlib/graph.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index da1042085..4051fc432 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -150,9 +150,11 @@ def get_label_position(self, edge, src_vertex, dest_vertex): angle = None # Determine the midpoint - if edge['curved']: + if edge.curved: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position - aux1, aux2 = get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, edge['curved']) + aux1, aux2 = get_bezier_control_points_for_curved_edge( + x1, y1, x2, y2, edge.curved, + ) pos = bezier_cubic(x1, y1, *aux1, *aux2, x2, y2, 0.5) else: pos = ( diff --git a/src/igraph/drawing/cairo/graph.py b/src/igraph/drawing/cairo/graph.py index 44bbef880..191701786 100644 --- a/src/igraph/drawing/cairo/graph.py +++ b/src/igraph/drawing/cairo/graph.py @@ -400,7 +400,7 @@ def draw(self, graph, *args, **kwds): src, dest = edge.tuple src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] (x, y), (halign, valign) = edge_drawer.get_label_position( - edge, src_vertex, dest_vertex + visual_edge, src_vertex, dest_vertex ) # Measure the text diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py index 28c2eb8a4..5037e4f2d 100644 --- a/src/igraph/drawing/matplotlib/graph.py +++ b/src/igraph/drawing/matplotlib/graph.py @@ -290,7 +290,7 @@ def draw(self, graph, *args, **kwds): src, dest = edge.tuple src_vertex, dest_vertex = vertex_builder[src], vertex_builder[dest] (x, y), (halign, valign) = edge_drawer.get_label_position( - edge, + visual_edge, src_vertex, dest_vertex, ) @@ -301,8 +301,8 @@ def draw(self, graph, *args, **kwds): label, fontsize=visual_edge.label_size, color=visual_edge.label_color, - ha=halign, - va=valign, + ha=halign.value, + va=valign.value, # TODO: offset, etc. ) From bcaa869a29573b9e0b23070c265b297ac76bed37 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 13 Dec 2021 08:17:52 +1100 Subject: [PATCH 0564/1681] fix: plot multigraphs in Cairo and mpl --- src/igraph/drawing/baseclasses.py | 3 ++- src/igraph/drawing/cairo/edge.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index 4051fc432..720404ac9 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -6,6 +6,7 @@ from math import atan2, pi from .text import TextAlignment +from .utils import get_bezier_control_points_for_curved_edge, evaluate_cubic_bezier ##################################################################### @@ -155,7 +156,7 @@ def get_label_position(self, edge, src_vertex, dest_vertex): aux1, aux2 = get_bezier_control_points_for_curved_edge( x1, y1, x2, y2, edge.curved, ) - pos = bezier_cubic(x1, y1, *aux1, *aux2, x2, y2, 0.5) + pos = evaluate_cubic_bezier(x1, y1, *aux1, *aux2, x2, y2, 0.5) else: pos = ( (src_vertex.position[0] + dest_vertex.position[0]) / 2.0, diff --git a/src/igraph/drawing/cairo/edge.py b/src/igraph/drawing/cairo/edge.py index 78de321fa..bf07ed316 100644 --- a/src/igraph/drawing/cairo/edge.py +++ b/src/igraph/drawing/cairo/edge.py @@ -104,7 +104,9 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): if edge.curved: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position - aux1, aux2 = get_bezier_control_points_for_curved_edge(x1, y1, x2, y2, edge['curved']) + aux1, aux2 = get_bezier_control_points_for_curved_edge( + x1, y1, x2, y2, edge.curved, + ) ctx.curve_to(aux1[0], aux1[1], aux2[0], aux2[1], *dest_vertex.position) else: ctx.line_to(*dest_vertex.position) From e3fc166028b995856553023bda618758d963e803 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 13 Dec 2021 13:11:37 +0100 Subject: [PATCH 0565/1681] doc: added information in the documentation about the MSVC redistributables --- doc/source/install.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 665339bf7..56137e69d 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -73,6 +73,11 @@ Precompiled Windows wheels for |igraph|'s Python interface are available on the `_ (see `Installing igraph from the Python Package Index`_). +.. TIP:: If you get DLL import errors while trying to import |igraph|, the most common reason + is that you do not have the Visual C++ Redistributable library installed on your machine. + Python's own installer is supposed to install it, but in case it was not installed on + your system, you can `download it from Microsoft`_. + Graph plotting in |igraph| is implemented using a third-party package called `Cairo `_. If you want to create publication-quality plots in |igraph| on Windows, you must also install Cairo and its Python bindings. The Cairo project does not @@ -90,7 +95,7 @@ After running the installer, you can launch Python again and check if it worked: >>> ig.plot(g) If PyCairo was successfully installed, this will display a Petersen graph. - + |igraph| on macOS ----------------- From 8a4a6d5dfcee0d84ce41b3a84d0368a1c91dd0f3 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 16 Dec 2021 07:55:52 +1100 Subject: [PATCH 0566/1681] Extend mkdoc.sh to make tutorials, fix TOC and some hyperlinks --- doc/source/gallery.rst | 13 +++++++ doc/source/index.rst | 7 ---- .../tutorials/erdos_renyi/erdos_renyi.rst | 2 +- scripts/mkdoc.sh | 39 +++++++++++++++++-- 4 files changed, 50 insertions(+), 11 deletions(-) diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index f3782f6ea..483092ace 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -15,3 +15,16 @@ This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-ring-animation` - :ref:`tutorials-maxflow` - :ref:`tutorials-shortest-paths` + + +.. toctree:: + :maxdepth: 2 + :hidden: + + tutorials/quickstart/quickstart + tutorials/bipartite_matching/bipartite_matching + tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow + tutorials/erdos_renyi/erdos_renyi + tutorials/maxflow/maxflow + tutorials/ring_animation/ring_animation + tutorials/shortest_paths/shortest_paths diff --git a/doc/source/index.rst b/doc/source/index.rst index e2295739c..145bc4c82 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -16,13 +16,6 @@ Contents: install tutorial gallery - tutorials/quickstart/quickstart - tutorials/bipartite_matching/bipartite_matching - tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow - tutorials/erdos_renyi/erdos_renyi - tutorials/maxflow/maxflow - tutorials/ring_animation/ring_animation - tutorials/shortest_paths/shortest_paths generation analysis visualisation diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index f980f7ee9..116551bf3 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -6,7 +6,7 @@ Erdős-Rényi Graph ================= -.. _Erdos_Renyi: https://igraph.org/python/doc/api/igraph.Graph.html#Erdos_Renyi +.. _Erdos_Renyi: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#Erdos_Renyi .. |Erdos_Renyi| replace:: :meth:`Erdos_Renyi` diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index 3c64a5ad1..984fed38b 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -2,22 +2,55 @@ # # Creates the API documentation for igraph's Python interface using PyDoctor # -# Usage: ./mkdoc.sh +# Usage: ./mkdoc.sh (makes API docs) +# ./mkdoc.sh -t (makes tutorials) + + +DOC_TYYPE=api + +while getopts ":t::" OPTION; do + case $OPTION in + t) + DOC_TYPE=tutorial + ;; + \?) + echo "Usage: $0 [-t]" + ;; + esac +done + SCRIPTS_FOLDER=`dirname $0` cd ${SCRIPTS_FOLDER}/.. ROOT_FOLDER=`pwd` +DOC_SOURCE_FOLDER=${ROOT_FOLDER}/doc/source DOC_API_FOLDER=${ROOT_FOLDER}/doc/api +DOC_TUTORIAL_FOLDER=${ROOT_FOLDER}/doc/tutorial cd ${ROOT_FOLDER} +# Create a virtual environment if [ ! -d ".venv" ]; then - # Create a virtual environment for pydoctor python3 -m venv .venv - .venv/bin/pip install -U pydoctor wheel fi +# Make tutorial only if requested +if [ ${DOC_TYPE}=="tutorial" ]; then + + # Install pydoctor into the venv + .venv/bin/pip install sphinx sphinxbootstrap4theme + + # Make sphinx tutorials + .venv/bin/python -m sphinx.cmd.build ${DOC_SOURCE_FOLDER} ${DOC_TUTORIAL_FOLDER} + exit $? +fi + +echo "shouldnt be here" +exit 0 + +# Install pydoctor into the venv +.venv/bin/pip install -U pydoctor wheel PYDOCTOR=.venv/bin/pydoctor if [ ! -f ${PYDOCTOR} ]; then echo "PyDoctor not installed in the virtualenv of the project, exiting..." From 514613a90fbfd872238e4d5e33a55aa947168f77 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 16 Dec 2021 08:17:59 +1100 Subject: [PATCH 0567/1681] Visualize_cliques tutorial --- doc/source/gallery.rst | 2 + .../tutorials/maxflow/assets/maxflow.py | 1 - .../assets/visualize_cliques.py | 21 ++++ .../assets/visualize_cliques_with_edges.py | 30 ++++++ .../visualize_cliques/figures/cliques.png | Bin 0 -> 99882 bytes .../figures/cliques_with_edges.png | Bin 0 -> 106100 bytes .../visualize_cliques/visualize_cliques.rst | 91 ++++++++++++++++++ 7 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 doc/source/tutorials/visualize_cliques/assets/visualize_cliques.py create mode 100644 doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py create mode 100644 doc/source/tutorials/visualize_cliques/figures/cliques.png create mode 100644 doc/source/tutorials/visualize_cliques/figures/cliques_with_edges.png create mode 100644 doc/source/tutorials/visualize_cliques/visualize_cliques.rst diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index 483092ace..16cb26f8c 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -15,6 +15,7 @@ This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-ring-animation` - :ref:`tutorials-maxflow` - :ref:`tutorials-shortest-paths` + - :ref:`tutorials-cliques` .. toctree:: @@ -28,3 +29,4 @@ This page contains short examples showcasing the functionality of |igraph|: tutorials/maxflow/maxflow tutorials/ring_animation/ring_animation tutorials/shortest_paths/shortest_paths + tutorials/visualize_cliques/visualize_cliques diff --git a/doc/source/tutorials/maxflow/assets/maxflow.py b/doc/source/tutorials/maxflow/assets/maxflow.py index 643e135b7..b28b81bfc 100644 --- a/doc/source/tutorials/maxflow/assets/maxflow.py +++ b/doc/source/tutorials/maxflow/assets/maxflow.py @@ -22,7 +22,6 @@ vertex_label=range(g.vcount()), vertex_color="lightblue" ) -ax.set_aspect(1) plt.show() # Output: diff --git a/doc/source/tutorials/visualize_cliques/assets/visualize_cliques.py b/doc/source/tutorials/visualize_cliques/assets/visualize_cliques.py new file mode 100644 index 000000000..cd8cdf6c9 --- /dev/null +++ b/doc/source/tutorials/visualize_cliques/assets/visualize_cliques.py @@ -0,0 +1,21 @@ +import igraph as ig +import matplotlib.pyplot as plt + +g = ig.Graph.Famous('Zachary') + +# Compute cliques +cliques = g.cliques(4, 4) + +# Plot each clique highlighted in a separate axes +fig, axs = plt.subplots(3, 4) +axs = axs.ravel() +for clique, ax in zip(cliques, axs): + ig.plot( + ig.VertexCover(g, [clique]), + mark_groups=True, palette=ig.RainbowPalette(), + edge_width=0.5, + target=ax, + ) + +plt.axis('off') +plt.show() diff --git a/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py b/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py new file mode 100644 index 000000000..faf3b9f11 --- /dev/null +++ b/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py @@ -0,0 +1,30 @@ +import igraph as ig +import matplotlib.pyplot as plt + +g = ig.Graph.Famous('Zachary') +cliques = g.cliques(4, 4) + +fig, axs = plt.subplots(3, 4) +axs = axs.ravel() +for clique, ax in zip(cliques, axs): + # Color vertices yellow/red based on whether they are in this clique + g.vs['color'] = 'yellow' + g.vs[clique]['color'] = 'red' + + # Color edges black/red based on whether they are in this clique + # also increase thickness of clique edges + clique_edges = g.es.select(_within=clique) + g.es['color'] = 'black' + clique_edges['color'] = 'red' + g.es['width'] = 0.3 + clique_edges['width'] = 1 + + ig.plot( + ig.VertexCover(g, [clique]), + mark_groups=True, + palette=ig.RainbowPalette(), + target=ax, + ) + +plt.axis('off') +plt.show() diff --git a/doc/source/tutorials/visualize_cliques/figures/cliques.png b/doc/source/tutorials/visualize_cliques/figures/cliques.png new file mode 100644 index 0000000000000000000000000000000000000000..a6e9d626f172f8347c0454974c1148cb2cbddbbd GIT binary patch literal 99882 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�`FG0gowi&!Jzy&$N>#c##5RQPIl^)x(* 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 literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/visualize_cliques/figures/cliques_with_edges.png b/doc/source/tutorials/visualize_cliques/figures/cliques_with_edges.png new file mode 100644 index 0000000000000000000000000000000000000000..49e4abf1ca037631a0b229576b7602f1867ff837 GIT binary patch literal 106100 zcmeEuhd0)L|3Aqrk?fQtGa1v;w1oL8l$Vx=M?BBEATQ_>?M+T}w;L_9!2hJO)n z`aO*QNV_N-x#&AwcX746eT_)V(#6rv!Nu-|)sef`Zad#_uoo2+6BHFVV&mfC=qw{7 zbnE|rgP_B0YoWTAN)31sN=G$gXCfjROTzz&Gvr_2AR;CrQdc@_;1)mC>uNyX(NjJ@ zJENCMk#U4yg1$Jp(<8WK#b)Ze%~VMxa|-uAzl>Mk`#e;ectgxS_+%LEe}ClYkqZ+u zeJf$q8ye*iN>z!!8>C-ewYI)KySG@h;rD`RAg#g?^@wBm-?f83-wOH={-hqU(O2!uK)h=BMS?W#{XVO^dwQ#e?Ri` z`c)#{|GpYkU-N%2cuf8O)==-+0R>t*It4d3H$x`np9)S+PP@s;Qw+0mi+J@Ib+g*= zw*yQ}&)by4Kk6tc?QVE^T9G2WO?mANPUKHLlk8`uo*w$|FP~lbTVMOuIQL_bnjWLn z?O}?wg^{YhzL%`xZx$xnvfc_tEX67-E9+(%-kF-7{*a&lwYT?lR_L*7zMq{(t*%^A zOi~CO>n-Ln$(G0pU24;#V6i%9JT~{IBJWnuv#L-IDduMazCJ!gAxuIq+ms{aQtq$W zW$183uzS-l&E{CG&QaFIit=QIa(SwhZ7%E)6m&K&Gp~3dPIdqOTMC-zG`qUgb#!Q} zLKv=8Qt;~TQh5?ULcXuD)N|vlVE0A!b|MX9er|3upMqOSPFH$Ac6N3O2!tE~Bk2Qy4A&>lP}bkqA|Y~IZs$^Nk!$|E_Xi4kv! zqdpp3y?X3g?X!WlEZ$bz4E@*K=H}KSVq$d@$?AzsH1eC}-T5~~PU^ifV7>jfPWsKo zoR@7?5ppRvJKpIQ-_@e0qUPfh#u`2oFn&S!0%yL7R@ZQ4P zbMn7uC-c#JCgZ(*>g?IGY^U6wUax=ARP5@Ysj2y~$f(&mDki4pi~C|tf~;hreXmt( zhHjLQg&bj>u1|L4RCjm3Sf1=y>~-yLN;}6yPo-?gbZ-5TpTEDf;{Z{iL;v@$UwyZ? zHm=;f8P`^|Zp(7@4sq(#L)zwuzp&E9MMa)LWkV$>LE`V0R{7XzzimLNu`w3Pt znGEU-Fvf{&iM#jeTn`_Qg^t?~olq2MFtHaoxH~!r+$+wLr z98I-u|A82hD?0plEQ9xM%biB;OYDuym*bD-&kE(d7M^R}5W%B;_~)||d&X{ikNk+_ z2|dTc8nTxfp?@3MX(S@y*%ylAS{e#x;y_|p{d-m zHM;_Zsi`TmxNX$i>DuqV;-wY)zPR(56}c3RoFuA~qobqi%)1d;x+ecvQ*&Xh=J@+1 z<1aTGs~1Nbek96!Ge&8w|CYGbt+erP`r4wm#J}Mm2kiSxwIjjTts(21 zBHvyi{~8k$U zJ;y?O^jCuDp(T!4^2D&cL}}Xgi+}5}Zn#CaudRDsy_(9lwHEEMzQiYK|65IRY)I?& z(rAZJcDt&?j!J1sSWXA{FV88i}G1NShU+`GOiE4{ur`7A1GsqZZN zLh{aX&nG9&L$Y_H>OHzThpNN+WyciTiM-?%-W~TmiMlnkzC6i7=Bd_Ije{}!bB$MM z-J%c&gHU~9PhP_Y=`pI`+-5)JHYeK=`uK5lHgRHbJk|O2j3CmFW7>PE#Vp!QtQtgq+84t7^cM#k&qE{()^TEFPy7jKXJWC&q6 z{va)li&OQ<{aK0Tj;fB1=M2XUFI!pp1qV~g?`$rT=l`qmdX@MP>l609r6tlZ+eA10 zQAqij6U!6g5u%<~Mb*{S#cU12-oHO0Ah6rglD~9grg+63dy`f8Z-4am3NG41KK*o^ z%!m-`gJPO-64NC;m6eq>`^zoDRl^SJmU$go7*^+yFuZ)jc)-7uym^!}&L=JGZ%sHS zy`WwBbPwy_1}3+AFOQ0gFPHTE;MvKkPEif}Nx?k)eE>dv3En65yFux=2miy@wD=c|c6&QtsB#89~}$uVy3>qT1k@1Oen z_wP>HShFe}6I1m{|H^B*H~5#R#9e*OMN3-=qtY^4;H<@x#fwTm0?3tb16ZZPCpR|FrR($l}=dA1=z_MuA350`_| zv}3jVnVHp4xH3&Y@4~Ivxp>z2gH;t#j2I=O;BFEUu7~>$S>ZY;sj7Mp)1BK1dHS&= zD2T%H#tp&v_^9L?`T=^Gv~qHCJ8trsnVEwdvm2po(tDLN^{T3{!c_^fu1aUl_!yPm z|Ll18E~CfF3`34Z8KVNle2+sJ<(@sFmRBOreJU>Yo~nr0PC~_{basv&>t_$=e5m(` zO?uE`dUW8c|F?FDFk#VJj`fvUq4w-cJcC0+RXCS{xw*MUAFKi?sHo@^D4eFd#dVAF z{DnnC=;Swr&!ncOdsT8qY-i$K?KzWY8^~*6&i6LkIr!73Pu^b=-@g}b%Fy++w6i-h z(VoqRs?1|va*yksc?mxXN&sGZZefAWHs1t$5;D_U#*m(_l6-%y`CEVg&H=XACGDsa zm-Ofb-_rFKaS(mDGuiH)pJ<$Wol;cvO1MyBKC|@gR4Mm`RJZxTK%+wY1j4&VNv8`B z4_BbzD7e+by)cr?DDC_ri0j<5`*juJeG0&`pYAQcA*u}A6BNVTOXIb*zP!GYZ<6ik z?0jIlr;zCY&-qqzM~9(_i8uZBXz7CsBem-*etv$HKckOV;SBH+iCm>fRzDDkHDKJo ze}CfeYATKX2P@;I9Mnmr=Yl~pcWhHV`WqUw@2~W_CSyBhJU`;%XL;#^(BVBJu0+b!eY{q?`-q+% zt@n0jG2`#g&Z;|J3JQKPr`$i<^p}uQQwyS1aJ@CR zyv%U!xgbM*oFtQq3YEw$)?@>|Xh#=;jq!at_i`#Lm576)kKgI=QwZE!+4anc?n&6`)8%tlN!3k65qVKDQL>Np%3zZiMrw}-8ivNm9kR5yUvQ$)Sg+>4z%R5gdNH^?`;PZ$ zZ?WrSzbrOchnGIt_CnzM0>j`ChT4+Fv*m4V;=LQM&nM6wl(g3@`~_ILO|?$T!BN!k z`@@y$YI$M4LtW%LM=!oJjhAu?Nmh=tx@wR(Teic|8&ns`qpeq5;MelnvX%D)aK?`6 z#%xmxIcoM)PoZ|%Tiq0um>6c=(k~ISrTThrjVu9Uw?FJtUuTL>U=p=ybW~8hxzOuc z(lTOK>@-A{V_r2d^X|`>F13ir{OlC_v-o(;Cv4J7IiXBK=gYjdV#IGgE0xa(58q$^ zLi~F3F;WKp47YUytt`WAxn5$8*$XKu)OV-5saw+wG8tW_IvGg(ceELu7KWW3P`BJ( z7ZbC~C~s+5^k2!=%Zw4W+{JS~egMsJeI)x*;opC^PB0wjj}Z?}Sjt81>nA6_L$Xm(eEKu&vt?Rw67kZ`52z1HH0D^yYee%a_m}oZMnx&% zB#DTMa`N%Y#-pPj8` zTxa%Zh~y6so?`vH5i0`0H5Hj-9!6pu*S2B*m`08wWt8#Oyks zVk4Szt+fsK8K%1#6q0mawb_nLoiVQY_3KH3?2fnatI*IrqrJhqeQi1x>{1kyZ$;ilPZYZpRV((t1G(Fu{XM79yu|>%xzkTNy zRjMf>A~Lwr@6BYAZAd8Avt_eFw?}{RX+*te`ug?j?Cn=%#S@PW?YC8#8Sy^~}dIgFP9xL{`U)(G$Z{6ba+SyuhwRc^ev)nop zR(4KPlV0?C9W7qg3Yc%a?^2mp$rROG;QY!86Icub8rxKtXj9&}W_IZuoZ61AXm{kR2sD$F#Xq#`=OTn60Bd!q%j>z5t`1O)YaZD{>2^OiTueQ%h>AZq=KzNx8+eVRG-(J<$;D84;= z_7u6!{(5sE3!Rr7Pjls0eBe}7Mt6h1K=-k8!46>F_d3 zIUafU?j8F)Z|aV1%~KA$zAv$#K1mh1jDOqO@U4px*(jkbjf{*Gk&yVcRKg~GTlm^f z`tOfe6w#f!a;<63Jrnrz)t`K6x`)Zt)fFvtnQx!!IujVjIp1BRb4yE=*zloh?S&o( zXWTiuq@<*sdu4}YT#m@ey#y6Iz{vPLg6o`=%NQ--ZMs!e2muaMraax=6RlANZe#Sx zk%Oq^wYGJCX@y-+!5(k#9q*F}63H`u-S=ml}lX2tR)aYS!T3;6i)e z*=BDZK=o`kHa1@4kH-!lKJ4=@=T&z0iIv%Y)vUJd$u09wj=sSMc=Sr|-%xqXdUj`f z>qf^raiFW#x5kkr9xGmN6`yW%rs@6oq^wV7XbYDk2J1eUO z;8CpD4LY;pyVo{XMGP_rE77f4M6d5fi&M@v^bS#s*bZboVM5EyT;oqpvs6o1To9S< z7B7E)|4hSdRx}7J{Aiv-|4R$6wLtJNa3lZ3#D4D+MN8pECMFZdNzb^-tEsDhZET#A zKm8uR>oM-4e(~KqWc%1^=Kd&V8t3x=`0-;evoMjQWMSAHivQZ(z1MhW^9v*AHn+BZ zL?36;)zx*GXbs-l=uhmD7i~zCS54BvW2w2gfJggH_xxF%k34mM^(m{k?W-o6Pq@RX za&+VUCq+e5EXur4G)&9hp7ke-Kke}^yXQ+58SQ0|nx(0(NK|OyTdYdTly!A_o!}$i zzklC*_*4uooLB46)9pB467r8XI|Qw)tcc9Ab$~G4aHY~GLsyQ`$hj{Z!&$8C>4{Sa zJR*2r*p~j&=ke_s9Hqeq`JMZtL?q*j{JP1z_wC!qcKTivfW`p^24ytzP5_XyZj0Nu zPnMRJqQ))`|8PEaYT&Py+o~`H?m2;;5|XSGOm_;6 z-r_>N!|zYw=;>CLmc->Bt~hRgnJLL@dVsrj!n)z*mUjKQ=IRTnYWw*wys5-FU#}FT9pIHg1t|sZbEKVx6xSsJWIvNVCV)ZwhV~ zx3(I5DJjXsm#G5%?0ZXxR-F`OTpxrmBxLBNQT_V$O9?6%B&EH3_kOy+w!kAVFm zEet#-Xi*B;W-otq2pyM(<7{8a-pTFL42^NkABlbJTR(huH}5Wdc>JPf%xMofNELqo zl$cpqMC>|+fz;*x=&J7+;OIxVLaL|FWcEYdiMJM#XArcMK9v<&yFKNka#YqY@6S_%3OKr%4wlN60tUzHlH~_G8-Dva{=MG3N*GqYC!2nt-NCbo50{0x{=hwh-RNFDT zx3!*6!275v2hfA9<_D_=QDzgzY*M|i;4kaP8dx85SJ>Edxa6L)>n}ZBu>h%HLY3hm z8U&ho@bK`}{j{`4aM6|~6QrCJz>if=d2Jfu=nYwDt8#m?0-YrT!XM@0Ql{Xjd?9|b z*{rtZ%e09w-T|6nAbJZZ6B7pq$5AmcgH!GcO5XL5B0k4RI=qy+zxp0kXg}&P6RJ(( z%&U(q#w5I+!sO)S&^MR0?-8ot%1keN-Zak9SWlr4q*7=Dq7cnAwY2!)8Q>|~axNCw z8GCK7Stlna8Z+mlrpaMdp3*pvcb;5RP-?nRhcoUF{P}jM*0#T71fv#v?2*_pc zSYuzLod$Pbzka>-o!Pzsrwo_wDhTk;q98L}?s zq~Gc(py$=`!D-ipWa~LNwz;+_>NKbXWvm>3yn#1|CR*_$^OU%)f%{D+`tgbyqB-v~ zxZBph;{S})J~uYmm!=n-kZ=fkK)kH$lVs&kC|V1B+tO&GHu)xmS87f>H{M$;>Ao=R zJ}LoRd<8o_)mIXS2Bw$UOycL?&{TA1k%mg5R02C@`o%5$Qlb5|JGwfV29KCUc7fdm z(2Cvajsm;<;#!=yoe17Hiq*@r?_E!r?CI(f!6OR@2y_M-L zKIypq8WhCGdGyyjaJ0Y))pw?!3D=!T&~yNs4h>sEvHMxwy6;PwJ0bXvlr~$ZyLYdD z3!t3UKQuH{@#Ij?hx}#Lq)%t# zBu?aRmadj*lKEIj0l#ZR@t%j)$amo#jk-lCGilx-B*@s1q! zgA#VHEGr(+^XU!y%hi^b6N!q7E{y1ZbQj%RihKZd$$G7R*7=;88oujZYo3i=%C|%zrw(qpGv>edaZ=rd) zpp~TaRMyo#z~@%a=I4WVlaYBns;GH(m1s3E?v(qkCr^H?uKOkQ-b~lp5YGzT zT~1&F00N(Hj~o>cn9A07gBJ7zw|L;spCoqmbvf3@4^R;Yr>5$F0^R2o&gEX8s}d*WW$5h>3wHU;{ISN; zNy*Aa$`(>Tq&Ys;NL*m2t5;}Wq3w9pq5vd%SGZ|0S$$jG*4DQP$HlX+t7aOUL}zCdGS`IHv83$@B2@OoB>OLr5;HUN zE5j_5nx;!D#{&p#7xLG@#6(DbzW9~zk1SDA;a-tt7jQg2=sHvrE-%_D=sDeLD^5i^ zTuc7hd1vS2+lb>A1wD7No}IX~0H9e{Uq8RNSexhW)sn8|0&XWMDaoxFGxw{^qPs>O z<;4E?r>{Q5Br$-*)(z&Vw@2>A#N4l#>Gk+%MwHQJ+hmhxWISF2iY45oEW;DupB$Xq1p-;27=#Dg8BY?|$ zquaZ7?Q*d$dFd$$zkaE>jSFZH^PJh$Stok3X9zS)uq$c66Zq)XNx=1l~i$}gB z4~6YIX{qRGnOQQKWeZHOu>n5Mg+#)FlU%b+KINFITj|mL2s@l~Lg&m*YZ*a_y_oY0 zl^87Ra(Xcp1;qf=EGUzD@Bjdk!j~A-O zhrGPHj|-{dJ6)ZfM8Yg>xEbn`uTV}K;%`x zp2Q)?QX_vdYO9WS)ARxvwO_k4Bj3r7^ykDHb7bq4S5=XEczEDfDE#>rxIF9O?yj4p zK>Ba0t77;^q-d|Ikr6ZW=A(jw`+z4vY+{nwu^d`Ay7R_)PCuK1L~8LKG3BoHCs zz$!qY=IKF2ui@sMo)Qnq>L>1WY;1M&L)FIMngqoGJQ(hs*T02SsDRB2N`gl3i4+wT z0Upx49nd<^ddX-GYXU$o*Xrx*OM7iffmK%5)jfilblKW^#W-xp`v+j;jmB5RpnQLU z@d!l;%ejwTE?#ipmj+hO5@!*PtK;&-1yAY$zzbnXN%ERTD#x8|&+W}cZQMd}+t$lV zV@(!H!i!wZw)t-N?m_#mhFmrG@85Magt`~vp`ftWe~23->7?#C19+?sVnjF}(3`+1 z=Y3{YytX&p#aOBJ(>2vyXG0!8u7XAc`P=T+t#PR7MNI?EuTyBdoYY2kh#sp{05GCv zeJO2-gFpl7@fWNIYF-$Yr%b$MTa{6?;$o%rno99r~N5VEq!1AwHT7pYb6G`2j zVpa~JPXZ?sFnaIv&a8NFU|`O;c6fHS0Ze-BUynnJSxw?;kMBVKnL0_ku-329soCW2 z$AG91j%ENYfX#mN!M)VfH=0u^ul{OK+7JUZnU6p-oLgBl(Q)E`uz?$7rzn?@XmVkCy`tJGr`!!oeXl zWn95)CZYmqg)ZZIGIu7FzWR|8qljFa8~6&}={^1x=wZb@C)4D_P44rlH{O}jkGE6z zecG1*9k1dcO-qU@&A!8@f}lj;4idzk;ksBTdh|~Y$*P<=Lj*TA6%}i^_WA5$v{~~H zzu*4N9&EwIg71illQ-rEhhM5ws=apDE^L=1aSYDM-@v-X4}Wrx40<*|B|o zoM-jsA#&QL(;gu>fFa7EjKLSO499B)J!62jg7F*DEwfo<+oJ{aqYa7LD_)wKkKX6j@(@8`R6;{Vhm9h5v#3Jh zz?N}1B^v_QwoImef7XXZ%GG;PUS8g5qLmMn%YA)myk(iPTmEyafk>{E^478!8^yG? zJmvg%*Z1B#+tAG^&J5SbsqWsrI|#y95aI2D;{-!)yZ`c|h65K#xlkkE#L1G(vU|%K zZ!OBqcVuK_Of(Z@E-0y}cy;gM;trW+WZXp?dUk17k{)km?K`u;u`d0f!>21BGoILe zB;6-wr$gCNDu_JG(6jraXPSb7LM8C{NlypRvT<{V?mTr|Pp^fOH^f+Rv;q!c8%^~AzBwvht+shVP7}I873?)pfezdJZ&g*wJ2K+p7yjdh_gCj4pO3dsb?u@} z*)IA^GDsg($s$?_P*zp@TqrOp$#-=|w({RJMKo1Yk|Mbis?{3NDrtDaiBe%GBvmMS z&_=Gh&DJ+Pe^vWf9P*&4nc4DUt{0t`m(us|$-D27+#-5RTNy7y%A=h;zcBba?HrNV z?&hLV?vc75KQ3fRL4_@nyV6|Db?n&td&{ddlMD9g~uJO2@UkK2FhhmtSS&?!%|t%2B&0DJafl zwRtt4^wJ`j^0-1D3JQMBnS**sd;EL5eFshrfv4tPuRjXgA@bSrqepki%8vCNvTbXo zV`cq`xITpH=2*(a{v3WSXH`ylPp6JO7gR5!3!bTE$JVwjOh@7o9&wywr#JB zMPc*58M z3Y*2aNJ%eSTlyxHoW8rgJ^U;701XX|CoOM4d3*aakZM9D{uN7fri1+?FE3=0P?whP zo?0LY0XaEBsIr#u!yd7S{>>gu0}cT1l z2B&Hu6~mJM?!FQ5;>z#etsES7`ey5Pl5w-((fNM(AOdEt5J*+Byb}M+WLyhDtSgYO z>k?$2!-n$wxWw$azGR8*AO4jMl~E^6!&utNSAgYgz*}M?Du`s`fnN{P0g~iX&pH-5&x*0yd&*T=}aksJw zy4%9Nur#h1OtmPE5F`|Vpt7nfAc*ppDf zN*0F#X`#{VPSaCIluT}W{u<|PS~W&G1;E8K?Qf0uoay`W`LoB$A6cQ_kaP&N6DU)n zqSP?QoUlgnQn~Bo8(Xf9cmICP(0vpqLelf`+8UQs%rY5S-V2%NA=eq5#&V+2{F`0+ zpM;kc6)1!*K56pYSg@&MezG-3lJBwHOmme8J~t_WLqo)}IDYl&Ri&|yxC(aKYwqRUbSV+E*#dwS>4ek^Y0$jiX_eLddkoaxS;LZ_> z>BpuhY$_BP0tEqW_bn@v2RTI=g`iG*Z4vefm<(=aG>=*Ia+$2d2ids5AVRwFe^Q3g=`_KMIqSQ0 z(n1cv@$?w&2N>G|E71kYbg4zLj+OV8L+RMKDp`Y0D2i$~(VDq<@$suefP158QxG)= zW@okS>;xboNl8fwqfz7be+OtoVIYAPxy!|09nv9K6z8#HN;@kOl9JW13vu8kR13*S zNt-@7k@)$&w7M33z}1xoI_rk|6QL~dHsR}a%tUo{w;i`P(_Fb5I&8$So~K%dCuU}5 z2x2B^XV|40u5*_+-IBS_U+tah+ReaQ{%ZK@`Kl^iuVJ4~BNO1l5Nd+~@R5hn|JM5fV)WVw+jTEDOU-dEe+ zv=PBQq_Q5Z>JpTksy<3mF0iyTt`Fg-ZgZ_MkI&%UUVHmmmXlX&WD7ld>e(-YIsoQ~ zn(;r`LDGgHN8;98nUnviefx$>-NM^dpS+^j!|7f_8}B@Pq<^J9Mf!dWdvc0#`)5(k zncku0Tx)?_olk#|(E`m$o%!UeYAa1%%Lx;rbeGlCN)i)om5ws0mr9TKj9W)aQ~)zBGEtg&Yp$> z1lR|Su5v|A%R2)!>h@qICG7ex&;;no0C)jVq)xgoT%G91$zlItTzcOfe`>%6gUY7j zIe^>35RfUb>+%DwJ$-MfCeQ8$(2tkbx;-@nIL#R0m*yqAvrW#19i{>r{r=vvLhk1W zPs+Qqef5R?UZKyKc9VVcAs!eS@{f#cxm2jGrgj-{DOOG(wLTwr^Bij7itbNlCZ>lE zACjX(m=@oqCJ0aGM2wgqJ&gXTc(^pvdobIu7FfW2 zX>30jh{sKZvCd8-;AuiyNA3K1G8grL#M8y!yYo%bwJ%1*n=)GTzfC;2aphy4*lXv} z4$U~SeA~>iM$1MI>-+83ePRp@GQ_6 zU?*f4eK^X%Z=fcb$USEJ6I&=+ztNg~N%9H1{4Z?y+D!56_Jz-SdU{fKCg=&03#1s8 z9P_5PrqU8TT)NfWFKiw^U|Gj;;4nf_gGjm5UNJ${EW zrcHnKZ#B;g0y#v|KpiAl5ka2}og{^#25||A?>~O<^=fH9=d`8izt8EmMkjHrIRe?e z3#2||WDkzUzV=$LK$3f(R;D-n_YS zG17?!mPriar|s?S1p6PzJQpyLpd;h##l4fhn53hVsuu3zdlsQ*IIAfd3^u=wIud!O ziT~UTzk4Dm$R}^+Be>-t)W+eT(V;kv`fm&}T}FSlwk~CFjYny4;m59kHsG>sP8#~+ zY@IrFiqIb*Vk65*Co3zf@`(8?LK|*#1H`g-r^;bQ!@+g>TgQTqOt2|QspwxJ{sgy= z9%mJ#i4XQ4PNzz?VJ=)m2nLE!&qPy+KD>Yb>U@F>v{%)#sRsG5@QCCzrz@Rbj6(}u zr^;l~g<^58=I4uM&uWB}_xC4)6Ij~Vr~$KO7-Z@_=+A`#yZw9H3>6*O)Nfr~YZir6 zVE3@32&o+%B^AA`Ux!qXto~W{*GKz=K~OZ$`GL+H_L7dbe}FsV`Hds;|NgZzD`gw< zC4sYIIZ*CPP-zLg^o;=@fp-EC#>lz~ir;Lnd&=32V|Mb|&(w1acZO^B<3KA^Jb2n} zGna3+M~(-wSC!ZL4LUS2@>d88#>=D~+#P|;FpAP=q_nmvJ8Npx*lA1euSG!f{BozP zudk1gNXA0ayg96W{yZT=iK=B~V^bs9e}8dD{>xG2SN3Ml%u6-3qeJTJUx<7%lO_7s z5UaS-@_N2D_Q1-o1is5xDty+Lf$f>)*b2+f$P^bx&i*>Ided8h0&2x0^js|cucdFK zMd{E(!#*Mvm7HwVX#1ylwkY?`WI@iGpF8kB;JU<&jY)w#O1q3vK%aqOzA)Cr3F+Hy zZ6VET3n)ehx)%yvwu9hLg6wPj{xUHvvaNyANY|O)f3S_Xyo8Voj!;WmtGkCsuGiK* zf{lwH1n;E+0dys9LBS_*oAruZZs7J3Qf07`IS7&f^aE3PNs8}^Ao=3AJjP{0IjmXw zOOKEN^r5*qsk*M*QC)rx*BMjOl8MNq@kMWA=x5m`;?*rJdw`Bhtr0xsrKf^S6bOWi zmsL$^swWB;wLTvoACKHx8qCFY?vHh7n0uU^7isJnf}T7fq)7uFKAgOC0H`q0J^{tF z3H=FWXsj!j2bt+>z>zS`pWzEm=&I}grq7Kxry(h0iDddF1P-7%F0+|txOV{0Dw>*g zmqy9kZNxx^7jxv^-9%7`f*4T?OK3jqE}A>XCG0xO@wkYK(DO$nI*$tK!lTA|d~zP$ z2QwTMfTZY1nag-HAzK3hpfq=63-i7x31|9 zWp^&Q%pHXk9pTs3+GK?{RS)mk(=Kr7`%3kbKhJq+9@@57*0n@Z8XDRilz;i%tjKX~ z;o8*HR0fJ)QA6?lwUe-g#B5u~vRh8|m3qb@c9x5%6+h(f~w;>1f(L~AGmLn0}q_h3ACroXHK5ds3rL+Pvf z`7;E6oA4IEjboGdiU;q4&wB$B==b1!eSHqqeS;78UF+)3U&=J3E@ly>RSSzj{)vV4et>ycT_}NS^*HKG>=`)up)IHh-$O_&E*{!p5?m z-f8%iA6#e6Stab$5@hbUA+im?1;z5ApjpxVXc8iV9*O6v!Rbur+-^=44_{wheeXPa z9smUqEMh`ALmaxZr|0#aC(sKC#~%JfWo>OJ5{pQKBs(zDD>x$8e?C#}F`@z3rvFwk z*c}3fCMt1Hp+j|J<5PHaQ1h3aOS*e{7OUC4k%^LT&d}xbTx^Btl&Syve7brTJhfVs z9|A{!41%*v$k`*ORNDUv&*JfKisR(T2%Ivn=!CL1TXDc#OLX13MERWr8tD%oK429| zprjG-6smZCm#yRGs*|Th30_?nmkxaV-Oi(nIp$6%U`IJQp;4zoS#n6Y_7`d*;6)q| zx-JA|1AF)6RiEqhuhUJ59LtID$zqfC!fkH_&&i?5$xt9)q$7|DL@KgO^JYNlT|ng8JpG-XRMIYoo8r9SMfg2e zVQ*>1OA#MG&Y-Ky3g0L+C502Y83J$t#4O3>)}{E06Y<$?{-~j$A)yeX0bTzU|D!Va zfD=SrU?;q{0QE@jKNAx_fpw4&I|Ld{aCT8uubYrW!ESwklY)+6w6LjhAY=~sGR{R|j)h0t3fF=4=G9AU+yJt31#%|_AQzG_yYQ6;~9 z89K*XkAII|+lp)Dm?jwveK!Sw_Q|^Flwo+HV$?>A6)A_BNFJI850HBdpgnXbVVvk; zPArPk_V+Cg1Y3WB$q?5h%FBaQpI`Apg+$YXrQp1nE^lw&SeWx*7iol}t1BVq!)_3F z4=fG|I6@{512V5sOK@j)LFs~VSq>4GS?<0R5sKXqY65};1R3cATyjD4FKS>&YY6`u zK_0x+!iFPH$iJEui6H_C3;cR5QDaR_P*hYDP3fQ5*jQvs!ajeNMoZP~2!Om!08a!X z2B%X>n(x!S3l(#JFrZ-qH+1RjsFZ-fBb3f2khE_V70b0|rlza&)$CAf1aW1`QSf1A z>wj@`BIpO&@>`fn5XJc+96nGOQN)oDvy2S>}8QuqQ z%A`L@Ip89MaxY9!g!%g`7QML3v70c;h4<^-wp-PCt7xW#|AL?UzbO_JSFc%;!n-{Z z5+gOfzDXRXwY2U`V#!Cq4%M~N&aW-h^%gu%+?km8)B8nQJnYa=HM`dUD%WbZxt7A{ zFQdaUkp3^sFD=!HX+5U(!<-NbB+`mBfByUlc<`X~PWjU%_WKi>EzJ)>ZfOyf*&bLH zXVoeam~$ZDJz~s6(3%w;!TnJJgnF=?oRWgrvu8l0zPkvVEvNZGWfAAUn)^SyeW2Ay zGipu_q&*_o5YP8->t7u0r9vu88(yU{ul3A1jkY(fZpEHdDE269 z^ozkPK~XFf1|2$ct-}y8wJb8;MHnkVbqa`!i-XMCCUh~Y?WmmGHUcy|$>=59L(Bb= z8;ZAYeHxeV zL)rFvmYS9p18y1}N?(hEH*OI@OC*HH_A@eaL*V0hA?YA^^ytw5T7{|pGWiMtNt(l_ zXmR@JkO^uMYO&29L~XJH1sa4%Hr^f<8hU{B@!tyV=A9VAZ)9grq%q$C)UyIZnl*EP zEp^n@KDslHQa3X~&)|)LT+1>}2(L+jogjD{FDS@Sg69LxURgsU1altCztJ!L{KZRzK$cD7d)?PwT%9d3dQ{b7$-F zuv+gHaX^feF567c#U)){C5=Qm1)F^EIq3=kLoD2WEX{dIEmK?DH=RXyvi{rfaV5;w zU|XU^LpRA!5^ef{f={t$&rVHk_6d38J8fDse2D^*k~9FvxLTG=H{Qj)f3Jg_80~`x zR0O50_n`dNF{RS`R_gUH6mHq_KMP|`+wkqk5ej(xm{v7x!h7zV>LXf&SOXnz(|xPc z3@W%4SeBU?v$eHy^s~#2{mto*p?iMD0)o(lfC2}jyne}_LWi<~0d&ckdWykWY>`ycPN>^nQsyv5pfh5J#OH%Dx|_IA)WkZ?$Ij1RXzvHB?x5m zgH6*>M5><@lDQyN!Y1RQMlg-QIWea)F)_-BiI+k0#Me7q2;q;X-`}KVzr9GaXU~Da zJw4+M{cTwnMc*RyrHmUJP*S4dGLgE67RMpOTV6y&cXM;G!Kx`)dELhz0Cc?!Lq}ik zujwJHp}5lr^ic_=zY5j45_#wVq!Pg`zO}UIgPs%6IN`v#o@ZcUX6D63;6Xv;Aq;M5 zq|y5zdeVIHAkk(Ekt6E=Q5l)_lF7%1Pn*a;oJDl=#kdSk10#R5!qP-wO~NHUwsZ6c zY-zuB83&mayPQ2lF$(MN@38v*(LWrf52(*cN^X7(=M0p(qY-p>IzWd%Fy{27O~bO% zRR*Qup#AWSbTPxBPZ7SRIM5GoAQ*Rtl8K4Q2?)T_!9tDbThoWP6qJ-}s-qgs;lu+> z!--@*7~^GvJV%eJJ!|R!NJW?oa>!oZuMiZ$HC^?TgM!0_7~`{Cl}PL;uY3zsE@k7_ zZLKq6YluE)qn$u!HE-2RIxw2JEdQ@VH7ulUsCv?zT}~jt-=EgdaF8wNf>}|J$jHxN zVNuZ!&21O<9+c3G2a@p*TZ9gH)cU->Zds`UV%ED_QK~%W{_UKDOn9=@tt1o+y@yGV z9%m3whL97X%q0JpRkr+JRvvj!bTWqa!arVc-I>v?G{CL*)X} zxSDa=Z83d$efj46WKLNK+8kk!Oaywl)6$qOq1YAOo#uZgWFd$o?bg}q%qANpWIqCe zgB3RC$&*dQ)9|wEXM47V)+NQiLgpN|{jn`5CudF=bJ<)S3UkP|d=wHw=hOLVe^T4g zg<;(gJZLDPZ~?>wopKjYYDx~Qggq8OTjVoWPozs>cH&puBE3WB`?ID+F6w*Pq;wF* zHUh*WK-^G(%pipCM-pg!y+{B2;UJo?pb1isbm?!RaVJ{07@n9|53?;&gzF0>p|3~d zBW?y78V6<}7TfP&Mn+(wO`n2;7H#^ym^l5-M~|yA&L=2wh>Ge`RT8J^9oxJ>{N8R@ zZU6MlOA(@_1-fr7`a$=W1B4|Q_q-6>KUub>@`%`J^tlD$RukG?6(KpkY6N0~amsz) zzMkc1f2Z8p{YD7^54qJ->O0#1h&TOsqcy}>>yb-G?Yy>j`{F(}DWxZDDj4q$hS+e; zXcjsh2-QPy(+PdGtLZ9_yY0G{VrtY*U~2F3VC6f4w>tFWzylicoJiJnQrp!Vv*z@& zu724k-gM-AD0cMyR#2c+U9Hc5>uP!|OI=*`EX<$dJs~_^_ z=XSc_)_~D|`}S>V2W4g1QxCb5`FYB`8zl0ZM7h^9Q7x+EWeRtW!!iyiF7|l97VJP! z^thp|BXTs}nn?md@TagJdM_P4Jz*{zgC|}$EfN8TBO{XIswH{Vl#>+P*S0riG3LGd zS}nKE>nE3ozF#6lM=(fo8;OAz61NB$Za&jb!bqr}UpXWyE>5r~Q5Imt)1Q%ioB)v4 z@40daLSXX>$pHmPi>bJN-=Lr%7iaP^w|UGR((h$H;tygD~>)y`dqDAZDy|WnC2Fzi@l_-?S_dR2x6QY7w`&%kWyG z1RE|9`sVBz1f)jaLBp&NMNX6jizSu}}ceNWv6D=+6QxVaMOL!4z4g=copB^cJWj@0hg z;QG3Mzg=)xb$j=VZ=_V0<0OS1-0&|k;9nhKx=X`Wqy6%w4bhdrT|1wh`4|r2V;U}G zUdVbTM;&FD#dY%J$zIoiS?3?sRaK3jKD_ew?ku`c5z5pcKgOm${sS>af;I|&{^^Y0 zssti>5KR?775Tlkrh;~mglr0nj3LJJ%@Z9L%@XeS{x-z)Cy{odEsJcye&oT1o?Ccv zlu-+qQDt4M2$O%)w7PMHem`+mNmb@86U<`U4h}0}K@L5hhSH06-P^=a@aSE?R)F6RaM2_x)vG z;-;pnF;ufREfEcMaakhIj(hs_3@)inuZdzLH%5_&p~yX*`MbvvkWR*B%ts{!aUdo6 z%LB87ArNR<(C68%{T#Ob>Ze>;Rki=%#*0k-B=rN(s-J%(a7ehE$vV7l+ndsWhlA*x z@O2(zf{3_C=^Y~3)j#>8Rl_b%7<3lYht3c2d~h04L81u{em2|iioN|41UeCiMQn|* zwlMZuPVbEtFosV2LjRQyv5>_ZmXoOsW%5lsBzY#&N5J3;-m03KTKa|_FFo5Sw*ZH-X$_Y=7VpibLS$cj(9GB}Xh26n23=rz?jNVKi^JAyHOw*Pk3^@M26X z*O8MVd}(nVSXlGL?jJvdG4=~W^IAJC;yQk4FU!dQtY68Z7h&!Nahe~w*5NK=I{QYm zIvjT&2af;m z$bYa>hWWI8miRK}Z?0e0SYK+gvfE3(FZKQ1X%p92hv`l7 z{bqVC2bD;na1zpPm?pFW6i)Odu>3DxTQBgTDk#iRH>gu+k&=;pXFA^`bPmQtUn%X@ zhI~akqX9n`B>fQ>j4UdPWaQ+8k1IjQ%^!L*8XG|$oSP#*<+V4*+*czl84C4%j93zi zci^5;Iu;iH>}>JFhrJPy9Vt8vrB+MYbA2y{_b`kRkJt4661f)0XZV&GANw+UzG>+> z+D!;kM{^2!yj=F5#-uZNm&y=nF+54)lJ9*Z_j`uH8`lCdtm@)uVs%UXF9`F_>Dpy# z{$!5x$$tAeOJQ(-K&#x#>0)MP#sVckfdY!Dle078(3}Ae6uPy4LuexyRNyKAulNv~ zqP0d$-TcBQ+RU|ojD7I}X%}VC9(+~=75=G0R=;CYC^tWUYoNP6FTCrt#{`||r%z-2 zl#tm2p+J$5kpbw^VPF%g^)d~ULXalOSIC5j6E5xWWg-fBH%(*s)16QmTk#xX*(+D{ z9_$-=p`v)f!z1d)kKv6;b}z5InnZSIpMlGtyc6q1L@G(bd^>zN8b6+6qGA;K1L3M{~E^YvD5b` zCORIT_1~8ZXU?^+k(!#i?9FG3ek44;EPvbNK6vmTPT;qJuN^Fs=Yth233v_+`Wgrg zX8)b=xfO&#Qw+?&TU#Y$@{-) z(BCB?jRx&bIDjF!%Wtk zP5%6oRX4B0VnHPzKCI^nITRhFQ&=9py1J+hKRbl+n)$}RJh|HewzlG*i5uRTi5#~$ zjOk1GvQHxQ@o_lcUY&IXPmsV6pu=hNK!vlQ_Tzme8`V;tUC&?z3A0CR8fCTJMw8#( zY>7m;1DPR3R6ba;$pqNu&=(K0kdR;penDpR03QB3?j)L23$olU^6LWwmytb*pULIq z4q2f;A$vn?rqzfi!;lyy zk}iZK4h0AAg*WsBp;EH&ZE>+IK$q4E3qHvO6C(qIgWoWZzT|ulE`xOMJ%r@!dW)h7 zt}6!F9_%~xCZ`RbA3(~;_zcs;l(p32Q$tA0YuigC~Tj!YEeJJV3~r+ZwT zF&F8nRfb7S2?>eTtczg~7Et;4a1r6$j6zmNdI@F_f%Wj2ZO4UUQ{f4F{|op zO;EF0F`dl+WADA+x$gV_agvcy6v-%~6v_xi6tYJmvyu>n1`0{pG82(q8QGB$lD#S` zyJ*R%j8tSq=I8b}ulIlO{q=hu$90^?)meJIp3ld)-*4+~%Pc?Ip7QR;h->~?Ggpx~ zStCzcd3Fv8Ps`i()_+Z@j`FR?cglc~i&f-w6bfc4ahrB-bPrDk+Cij4SdWS<3*IP1 z+YnU>AZ~QYH~viv(EG0gk4IQZl-PM|jCa-ia&q?G-)lJNAHmXx%%xB*RlkRaQZAXi z@Z27@!@tPpXzEdeRGK|4Q7H(ux+ z^th9&SN@v9R|#3MAo^y=o1dL~MZe4WGxc1gQ;pSfs+y*WV0Hu?-L&ip2-83m^IW_b;JFI?dFH z1O?t*l#x`KveWN&u52E&+prcE+#n}@&Fb+L57&LUyU zT$q^nIsVZ7ij3vRJm&Z`t}m5^va(0WAhIYrQ6)%|z2CdBcm+`@0mIQYxv~nFnLI`` zo-9g`3%Nmi11HN)>vryoPu^9!FGexdMpe4SA9Ccq9kBUSGQ}!Pbp!2T$uDGvck^WsJAz=+J3(n$)?oq*XAz=vDQB>v0=G>491>gnV_ zxeGAl8b*WHCHDOZxc(yJ;xuqwlgP6^>D^BU!lB!}%Xy$%dLEHBU}F@)`JZ}znL~*V zk}cv)M=c2xhu@V?*D1gn?gR(lgw{Y@Ts#>vVUXM8QK5^*FVx~jz){>)>E<;3tr_*^ zd59%A!cy@rQ8fer)EXQci_7cPyIBgeI>4evk}Slu+=8?WJkn7}<-tx6(IZZRSj#0S zdLwY^JNJtMQ&9v)2SQMl&L<^>4_uRsoSf6o_xnGVI%sQ!B=lrfktdBa7Xc&zfGqXl z90A$)q0DIyuKp1e351*hm)n(lN)j-dE(9)foA%hjngxd=C;VFocDM)ry4uP4;&lT6 ze^7cI#k!U7SVI678V&0>(R|$3B(@JOd1k2plu|ULZSv4=qw#>HEFK(g%qZDoQRnWoodTdz@P4{F{5e zdMWJTX_50>2h%2=7{!%d^mlZ!y0N>5yB!H@D8h)82S=kY&pP=nl#P(w`i`ZEe3UTYEdyLX=k<(#qYb*=a2H10_IXBvkG>3CvyvCR{Pm6^6 zq}M*o)GZV;<1~6C`wfctYg@O@dd>zZL*L1}YyBtgL&-*a7yg>s_5>8+@@Z`sRi2_P zAJ6;p?t`4%Et6lm@XO#ZBk_*$_O7r^5lcD6^k}IiXb*IYJU5oIx^Xh-L*TRGchuRL z@RdY44d=5jmQ;bWkknK*>Api+z@s<@>ZzuB4ifrDjuP5l-*6cNDe#yZBhmc-*LOOI<5|;JrS0=DL zNiha=8Z08)@#7aa@&q{4uz{G4ffnaJmehz=fSstUDBXJy6GZzzpJ0-m!7Rh7>r#I9g_LaH6(cX6aD+8}U~h<)^`l1mAib zCuxX8aoE1{V|ZBUOXbDNN{jmSprHD=AI8Pe)k3WaC2m?X{MF3J#zv56Ee=Y}_9@tdRJ>e6XE=d7SX>3;?~4+e~Up5F>c;aKrHYLYrOG7^Brin@g4MB(92 zQ2Cu68R2&L97p55&erC_>;B*ULJqccZ3%TRgUgd0dUmrqgl$`D4Ggu8maxH#fa4l} z&pKzj9YoxQPd{+;c#m)S?e#}5@3Sq@gY<~7*EoI=ThsXI6I9IFK<`lh)_Cr+Ieq0P z&L~9A*_C%?f+%_upQ_z+qS~DU1}TZLQ)%_UE z!5}@|C$5*@o~A!%#kZg3*s*J9$D~og%z70;Of`(my_&82&b+vX1_1Xe9UUF$d2us& zO%%V#`qiEL(Hs}gG4reRJV4|dHc!H&aG%k5@kBst1OoSlP4DgEneFVfUM1i3Gmmd4 zfQVN5InZAJmo@aGhKf{G*sA!2Eqv>1)>h?!gm=2`x82V6DkXZaW^3)orngaMx`%)2$i!&aoC}@MKoT^Z_4SoBjk*X$s zX)BH#A4m#n4j@V3DYQ8B`1H4#(N&70{cx6^+Y=MB@4%;xI zfN~>GZ~E7cv<>vm|Gs{S;a9p@Q(Uc(tbOyx$jIw7&HG>6c0%fW0e58XNy-D4eDCVW zgBH1;_W@3$9}m_PO^P1&&Q5&)t{TCw+D+|+&qf?A=#c;@G|o*(+rlObJP*Q}qcvw0 z6%{jtw9yGG2x{Du(9bp;zvxBGDyUW@8GHX#BzZ0df3wyWHol#z&bII5pl^2exG4`e zT3@oG)^6U=umC*IQEmg>6pvVb5qhY=@2zVV#Oe;;pSd4hd1aKtUrB8UiS{lzuh1ez zhUP^y$WGeh&lEqDD` zti!};QrUB7p^%83=PQttYsqsh^gYFqG4PJx7m3s1Y%qSse;(^4q$qq zS71?BdY#hkzJ9j#XnQ}fASbw~84W@&N^wSMrOSUeL@=eie-tvD;4W*KQY^jj>-6q@ zrU5Xf#m0&#Gx8ia@b^W3NaQAfmK$+eyl|P=3T4!B_E~jRRa_u*SC5Cx{&D}&9FV># z6I6F|TwUjpH40HqSGC72C@JPzaRU%($A=Syxvd|J9LQdUFNm;Z!3BVL#{G-uac8q5@pKpv@i36da|j5+`VGR3PvT0?zi-4L z{t~ZkHb0#E5I+PkSzaqxz^LMO)3fSrxOFQXj(F{P4;NQgcI?T;#`CfAA|KW-QwN8H z4A=Q7UTA-NZ9)87bL!iUoJCzEvXSkEM-nqs1qg>E23G$Y;evk0r?k|yywaRvb#-;o z?Nkqe4Uo4pZaz?TEB39Sh_t=!hjJrrMa2To$JaZLSaNP_q95_P{O4R{l0v3v0mC-N zGvRmrlRUe1(oUC2;1+AGn%jYwr5&rY)^?)SZcE-Mcq;ru(j)RJpyED{O*KBvI088` z7|C(-oOc(YE^B&t=(M{BwLb&b)M%2|j{(#lhXE$8%#ZSO&CJj5;o}Sb^@&>UI@&CQ zo|RR3Lume>Ivu+DkuFghITFawGA3z)I9Q8Bul4acQ_0=C z_F7H-6>he+K|39p;uyIl#XAM=PWCcgx%lMr#^p_wRbu;X?**7uz=E*)ZbdQMkmzx7 zEv1{sTo(~U78LM|_a8n)BqX$8cS(l!hJ}Riqm}EYAh}f6ATD2-fWq|LxwB4#-T^bC zB`;rBL+)o`!3*GE?ZwaTOk(T~g3$`92Fs69Y>(uWt6|OsjvST$f+*eFz4~Ai0gR9{sl9OFcqeu@ zT3dGViNKXeo~bZ-dODhK)E0#R**9>D`kX*TEs}fL3IN}2L@aj?y zJS;9Q*vjJ-2cM3G=klLMU9YfbKB#~Oe|xTvenMkusVd{XAV@p~?T;QH@Py#mU)@pJKB@$h6x5s0FO|A9$qIn4o4J9c`=q=i&cyB;b3v22f1i2?q zDBJ`S0|nuq>FHs7a5!*JAbSlS$bcFz%CH>=Y+y7te0rL#Qy|vUNj!M=yIzo&{?lC^ z2hq=F!r2VXEobM-6u?yym99ibpglOdfJr;(yCZANJKFgir2Vz6R#(2*jDdl!EHw?H0G63Mmr3!cja`;0)MyUo<) ziSJq)h?8YA&b!@T;nK5Ld5&sViw@99;U8eMGQ6i*d1#r-j`^zZ@XB+yoHLA(!zRuSeS(^U5+v67riXz8HB4Wy$pV1 zR<=1>%O6<|gznS#_#a6`I%L!5p+JuIThvm324PV`;M#Qn_JRQN0v<-8@U_9Awn#CB zN6(Rw8M*q1o=uo^OCC?l(f&e_t_-#e&_531JO2LEKo8M5`QRjmDf+&1>T7J^Fyowe z(mcTPnHlZ|C=Ct0_gs3K7ciM07@QUju{+1SI8AivIAF^r%<9nET1v4m$5uB6l^lNh*6MYna~j5OSU0k;U@e* z{G@bn6CCT>v9Af;zH;O>T$>Qwc`o1QU$Fruq4fPtyupC^*Ga@}9|dy!0{#)ys3a5v zQIHRyC}D;M9|s~Jh(<{B@wXvE&j`j+5*UdC;W4Nr~ul&iae$ssm0?48Bt;iK%# zASV29(2;m!4GlVIOEN&|jkg$-+M5y37AW`X2?bT5Ep|GiK0brr*5i)1&vH1J`r&xJ(%5bEG^8mu{P6cyN!#vSYU{zV*(``|TDF1!n!z`vJ@(of@#QLJ1A-06p} zXESD;0-hUZBQ_y{doZNIpy-0m<;#~(B0vXPJjCtkAutU(A%c8ZP4u4JwabEoLo;y9 z_9q)X<^i=tb?MA)W>$PwcIX0vQy`W9O|3L`F)dT4=R2ZB?&oM~p4_(Wo@iyk^8jD! zx{ghp2Y1Ps%ZF^BG|8(qpE<70lY~c&dTSIPnVbPY+PlkLE+LVl4gg<|(MF;@g!bPF z#wOT+k)h6u*aVn*I1!o&8NqQ7HY7d+?;FC2P2-Ed-TuqCYQVz66<`5a)P1H+dc1{* z=78|bQYJ543UeE8-HF04j(PY|ksU%q6LDZ{LAo-05(R~YmdIHk3=ocZXq<^w2W2L! zVDjKUCm-x|wBR@?nStp-15YFyC=iaZFXMTK? zlwwWwteA|<*oZwnFzWAKWjzL=S){qy2Q zlj#KC?^LUm34pX<;5kxqN2Q8M@QQ`YZT(*^d??0hz^t~ zSID|_BYj)dOu=x_-C62s+Z*Rpb`)tzr%s>Fc(8qJ5+>Xxq;Uh?BAF=gCjpGpMfY5N zO)*^`!MEr4?b8nq3uuYXAz2-`{(`YEP%5NDI78NO|1oi>F>*~y!$22gwkpSckaEFE z4ZD?MtnOds=~%5(%CNCj*XOb=l~pOmB2oh$jS=_vxVg{lmRH=Eyl-l{Yv&IIkCv$s(9aYi9U{XFMLGX^lr%LFl>dU&L4f3%iXHhnD@q9s!BB66Jg{-V%j@oVj~ALhADpt3a0w_5@m%H475$67l6>IL3xO)+gntm59k(gU!lON^b|SyOhx)aW0S@! zlvLz{K&OXFnbdz6u!ctbD^5oMSzn)0X%jOXx+F(OM~%`-z{jv=j6JxME_R=hqDT?t zBZ|eZ%l{-{F^mWOdO`N47@YQ?6GBikt@s~72wJ}^vSOhho@$dGAZkpK--+_n?tK9t z6Q`5~7VvHVBQ^_Dg6L6+<-Z%5*<^nsmKcR(lKBVXqbQYk%;MY}wtwgwgM|r(V|HXJdM;hbkiUZX*z!iFyJ~$>HkL;t z^TjY7ZwvG0`l2bvVu#rhDfpBU#CQZSEh0EwAOHo)$iVIOf_fA#*Cg)j+!?>#*5^=d z(__`~%NB}FIVQU%KQ-n)lKW@XCyEV9^7TPC4+3f?5kM%V^ongyc=Y@p*rJq)wcItJ zlA)Wl+T1bF#|zUioUSaatcsW;g%m{hj5;GgOeFmU1S?tf+qT8oi(VjJP6$>BaRBi6 z#?70N2?<(gk+U|2%6M?{lq6UR2^pFQ$0zM&zclgAYPiToN?hV`*lC?EIaglcA8W3Q zjXbz%|Nbq<+?nxJ(=2A;^vxw|esvG(EWcE~{MOp|Xv?0aQ#r^Bcz@DNa>ktMV_GP@ z67B7eJ{1)0Mebbt;=?BnT1#hAc_J<$K_6!lJm<%Oer-UX+|b~MD^mhD z4RRcgX*5ah*4+rx@IB}Z2{VcFq7EHA;nGouLxPF;M?c6Dp(Mfij<}_xxM2!PO5Vk$ zYo!x>fB?Jm zy&W>0@Kh@|VWaM;#S-7qjp1k|EU-Iih@=-$!E3ohHen+Fl z0UzldTsU3j2hLPvJa)8H58FQTR$*3MP3;F&9^Nqqt$DP>ijm5jT*6bekSH{wIe>*b z6C5RxCPAA=l#3{`f`ftv!7~yOEVA2CENY!Rxl1_O7qo&Ez<%@vkP&2i4xofZYmdJc z@SfPLP7O98xKl~w1tO4~!i-wIG3*zzPPZjjh{DG6RWk9!xNV+BeP~>q%ptnL?NHbr2f9qh(33u&bD9l?bZrz-LLwDYgip{1AW4vGwF>Aahpk-KR)Z=SySVqd^WTgpurz3iaS;JRP6l%Bso+*f#h zIi#Zc)&FvW$Wz_zC)_gDR9N*2eZlJ^-c&w z@VqE&`Yt1Gy>R#Syg% z-ryni!-r1>f830rq$>W|pYvENS_|5uX-x7YKNCzQWR8MJv^?LPb`}2tb4J?=t@g|+ zIR7tYSQQn@+!EwGyi!uE7^{OG219#n(D_3x=Z#*D?EJG;A!&MiX08&ZrVw`cpvr;e z$`2eoU@l9vun3~l1J#VYZppOkFcyq2@|#uNWHT_Je)nX<;UjhZM>w7u@6GhP6^J_# z0PT2}GBbN-?VC42z&Al0gB%3a1i=7c{P7^Ij=U${P2%Nng^@|Ar{ zs`g5ENsj%jo?SExX%V<(^H?Dxe!pb|BB)1vBC8xm*>_zpFfWZ3eQBF z2ZhwL3vHZOgG9E7{;qXuyCAGkIQJLd?gEZV?HqmBu73?rTVaS}Sr%fw+c^_#I}Ud` zEu(e=;QGHMtKin}kzMS7j*31VRUrmDYL zjfUoAeEiB<@d+hwX5=jlhu+=a_Q5C=1)YKcowo5GBqD7iy z%)S#nIqVM=^1;|K1M-36a08^OP*o~Jxs-3#0fh_Yl8$j2 zt;Fk)XpJcwzb}Y1Ik52@p!H5wEpCd8{9R-n45<VJBLE5qH_ir!9$;1E-Nlf@q zhMFdzq{Jno4$kuM4<(mkJuSMRWF`6N5ch_19tgqE-2#xrXcJO44HK#uRojgX5W+hB*z?q;)XRAQ1D2p77CB z76KB1$G0iZ!=z`@x?%EgpV$K!U2xs}&h9;6CL0v=p5bo*{dKF! zs@Go*#AFqg+Dbj&_1E{@klU9br1h`uwPe|G;9I|GTWqKt0S z1%Ic{54W{-0Z)Yp8rDdRFJnM_5iHr9#GVG7p|DBb^4TysbfrX~is$|8n!uUz^%H$L z8yFdPK?{*IrX(23eP}aIXhtR@@N7=2%c?C^eET|q6ae~6X-}$)ZD?p9DYd|(5O^62 z87gpk@A!BQBtH;KABr1{rr%2<#gFUU4I%`N8*Gaj%~DzP$Di#a@JiY zP8ZZFBw=>rM!RuegUy&UhpkcOI=L>#*e+gLSI5zj^7nw(!imPGQ{#Hh&&Rh-h_!A-&I=b8@|1kP-m>q) zxSsWxEF8kle!3XLm!!~hRxhded#OFY{QjZrt9OV-yV&V{VD86N>mP=U(pNCPw64#@ zW`2J0K(I-^M3KHg!1g0rui?(U3ITio)+44>=cF}hIa9lv z!3%aBP}kvTMD}|wlVRFH&|0Dm&ID$7PyygI*T1Tmd23;BHXYj$AW&co61 z7&0dnfPp(DkXsXvalfl4G1wO^o|cM=62wZly760H!K;KwopjKe1UZ~vtVZQb&-?nd zFhLJ+bLdKl`p*NZ)bw2XC5;8n$`XOk*Nn67CH^QzEGFmoJ-=cKl`u0)1aO&V1d&*S zghg3o>P3N*3<_!ja&R7;4?gU99J26_h@m-UhhYI97Cko|@9y0f3yY;@LwVe@o5n6sF4B34_P=%#@)Zb|6hA=_1eG=n+ zH}&OLNM7naj39BpJFT9tYULgsg18jnAjJHG9ZISVQq%%KC9_VF8Qy&z8BxRMq68Dq z@9?D(5@cR}oT`C14J1%i8YU*%VP&2MYW=d?0c;!ldbWaZ;HGD_qc1{J|r#TQzDDv za92&2$e9;W5(`HJ=Tgi;UzH*m1g=$F6DrHFMOu@S%(;mgs$@Z7?ZcAY6^p8WOB!XKD4U(k)$`~ruEU2{bof?5`!V{^O`QcJe1``sav|2UB<7zC|dw2EZg@ z-$9rzZq)IjFr0?|I0$CYL^^Y&)_-DQfq=CG&B>p+x${UHK#G=Tl4ge2+Ug?jaH+!p zfxBUM^2VWiqUxrLhaC8R6tk9?=?uLFcGiyMR{s0o#vx;VB>L50;jrMkK`(E710d6i zs03iNqcwVBNEi^j0!b)@SveGuSfB}~F>ccaj7CN6M2PL|Y$5QMHysAh%8mCdkbzo6 zYJ%n|I6Pc8_Ns8@BM2YJpiKyk)B(tmoj^!Ci~lB|FGxyQlur~GIZd9{GTA&5YW}W}22j}{R0|Us>grNhvD;xs zltfAcx~%m4d5GhXC!_!PMkV5Qi<-%c{p+En7 zm=L}=EzTY~W9D+I=4{QzbjWRlo?YFA@P831=xL8c{=W7|8VW&G#)foFejHNjxrO7{)Sy13c1p@@D`j_ZiT z5R=dZpatnC_pZv)lIZ=0K%s_G&;SxpT?iC+n1u3EtkP<1Nh?dTzxvH*sP{OHb@CAz5Lm@oG2f}R5{ga+Du^>6wWVF$r_@a5T0yN_9qSZV zKZ@bHJKo{GNRA_dMtGLQomu+G)b826&C-YDgN z%zy2mmT&bM31K?!av7;)sMtAyzTGr$Bli^ZJJKZ{30ss`dSO)Cr%}&uO$3g zL`9HrjzNY4FB)NE>?lZ;5e|y=Cy5QJGHm~jbf)sg|Gq?Z;_PV6iX9|Gm#a@?tRfwGg z7bMTmpNucO%$0}gv-^JH$N{LV|AFzzyNmcpBra<%-a1kU9z&!KTLYFLqieCZ;W3L3 zm*4H*apy7Ra`@LDbIlQ z;JCG<4~@c*m8<#CPQtSsJ68O;Cf~A+)6sRF7tEtk70#OUc%@v__lu5-UYW@I#*|$WVa4|3<{0M~Z1@9IfJ{tnbF;uiS)lm@ zG(u~b?CusQRf7E5a9HyXT&%6S#YwX%fQt5v{<~m=6+u@9EGb=hFT^Rh!?cTjp`_ez z*PTxqB-}%VI9kK|J^VY!DcabaRKpv%kpmv8&o3+$SC^^v!EaYT1Rq;ycV7}RPHh)Y zoZ(m~OvK29ZKV1E@6@(SY1Ga*S#ce3r=>Kj6ALzEK1i0J$o)MHA#B1D7v6Z6l$2C| z#s!~nAP38qA1YddD{H{+iQrEye8=6uK=qJk7@36Izj0-0>XSpkjX#_5?&MovuzukI zeU_UmQE)LDcnp+^%3yxyz5~;ROg1XpU=QZpT*FBb`Gg7J9wY{*@J#6+)WYikqQ3oY z_TVojWP-OetT6~cKmNJ=eC?a~0xz#0qjhMmkXXWxtA;55v)jd>+yZ)Q35-4anBNE{ zx^+D+AH@`h`pKHTUHq#vY37U<{lk6o^Yf$j+K9b=t=3&&S;cya8>q@&?_!5=!@S(h zdvh3tTCh7_y?m+h%=$zyvNd6zOM3h(Qs54vxP;3*w+o%@RD|nkDqbypiVx042$F*! zIo108`2#`ugj!%w+NBz@O=E49ju$f|x?>Se4EynuZ5({3o^1>A_m2l3|D$#L_U*4a z^#ttgn>tgs=` z2Gm)au}HL(Ls-^3VUGZ27PG&}4-XZEU|`$6eQ))PZI1>c%plekCJOqkQui+P-79x7 zI%i`t>$Fj4XiGAkxA_LU^q)Wr<0&??2R@WZxpiZp%t_r`9=ScuQO%0ytRmRjecvA~{oU=xzigf?!Z;rB)K;yFvpgvXU9QUmlWp4*2l zPf_8M`>=i|_U#TIiSt@QVwC1Gc5OZ=Qa14R_-dM zOpaI$`W{?0rzimT#o$u#j42d!mu1~^FaLHi;|yM2NJIpmQO-p=dj0#-4hI2%WQlda zjc;kmxm7H{s?pH8$s&e7(GVXM>Kqod$*kms}sKetISutryyu#KpOx zA4YzxP87U0?Ps`UENWY#5fCq_@%@ZApk|^k$40~4iOcl5*Zjr8i$}acI-M#y-IHyw zeq_WKdC#a>x?}eY%->;GzVRaO?l(hU3Y?!WTeaDPAHzPJJrY#!b#FFCkxgC%oZfx3 zml`-M3@+0KjTI}S9AQ<=&D}>D$nm$@{<_VmV!xyPkAf#FsHn*4$uuZN%>c%c$NJe& zu-{C7FKf6Ui?;q5tW%hCS;)vKB^XU3#>=ZHY`kx;H>>DFbBO4>voqF&zf#h$am-{!;*K z>bkonswy(3IIy<(5CoIvY$p;cx&d8SGVr?bei8qUCg5UZ42RK^tQSt|$bBq9(4Wdp zSM4Fm&1`J`kIYUKe>6FHYD4%Aj%yP!wBUEOC-Xd_1vwS{oylx&2$r1wJdU~YN$RlI zjwX~HJr$<(Nh~ZwFN5zNdzghnGUdiq<>L?ab}SEY5_^aSi?Zk$=8R)qgCp1JpBg{- z(yzY|3cUR5Bu{OO2&brd?`HPcTEM{gLFOU>>TRkqe4M<8|l`9)dKJofQ_;2 zPP>7a(Cr4rUP&9j|JK(q(TWw-K{f}39U-F~49{-Op9Nr=NIuvbFkc)DcZQJ#83BPK z3Bj2C9a(QfSY61|1DUw?H9sudSK(Tb;Tp>-FNrzURQ0LJ+NnqI|!Egw~{}$GiXD0-4POCton&#xz$G)^5T}6Ui++2X2_sR2Dh! z!CK9savQE0Oi-sY6gIl)s?PU_%oHXe6*$#kXA!C&uu;Tr@1_Pp2+Tqquy9YsUixtr zpm89B)6rW|KrnX?7@_m;==#wM(0h?-;RwBh9|M)2#-yl_`vq9zYmvqbuE_qT>-Psr z_>uB*AcE=}8v`Nx&-m{HveaQb9tysR5_ymSsEyK`I`BS;>>uVK)_o_Lz0q*LqFLaA zjnx+x0Fur(`@2Otke@=>-57|+2ZV-Ut@ZJsZExxAkbVta94y{-_TPSSry1`OBuDt}>LH|46AMLPNmBMi0GBJnYwPdtRr|erCAj5jd z9!0}9`#VHQ_Rqo0a5R^Z_{*Jt#Mh60!=JyI8D4`>&14yG_uV*Dhl=2g3jjQinaP?) z8FbzQG=MOW|K^Y38T+d$9^VUt>qcvG_Kz#;WQPM422o=6L67s|4NPGgX8aqkw_ed- zmxxB;FsFosLaS&Q+oy$TEqW(E83S`SMV|aYKM-_8EmYMLSYN76zFVn-S87k-W99{S z!F9>?djv%A!BTH{d9IM}gifQTYkVif#vc}!T5wJJ(5CeE_y^gOdJs1bNt~An>Xs`yVlD8-Fnj~og%Z&qn|hc(ZFVwtt1_@ut*+e zU5FPyHn~yI-3(<8v3`-31@J1ql-|N3rUMJ%3E*BEnwVHJW)dIsRYYs_S90$7kP|cZ z1};9x7zvWwJo|?UXg_IEA>1TVeAw{Pt}SXzaKGaIHj|uno>iU`id-OkVNhD&589(+ z7KGX9zY}ielS54ebghJB{!+Q&zpTd&@DqB^y=(XZ`y-k7UtwwK9)~YyAo{M697!Es z`R5XWbdvdjQldaZCQTBab!O1Ntb42EA*x$pD<* z>{P1_U>f|jau1Pwl{1H5EzL}vgSXMTd2_%6uJ<2bBiLb~-3c!%p8+0H1P3FVQv3qE zRF2+cbGVkWuKHs6Qk2scV3;@7=1vKTX`T|6EnEEj{gLgNa&kZ9#b1GQIKGaf5}wEApCTi8Tm61On9*fOkSlb!V#GIl#`swAONuPnk8d(D?$S^}w?jy!V~T#aNZo zPu?7v#1!u+t>~6NQ$t%Qz*{zzyKudL_7#mA){Oy#JB&;&d33}u0skB-Z74tKRk@ZA zG5b3t;uSIQPW0`Rw&(T6VA(^ek1%D2K$_qVs!E>X#cQI(BsC?8Y$D%d7jiYEIPX4` zb3M+Dwy3M?1iDNC?X8yz3-s=w2g8Tm2S6?98WN5ni+rY^pQxpz9Pz}+2w5de!7(T*OV#os4&Mxw~A<+;rdh%i$SI*0~ zh5{gNqhQeTYOxd!3c^kz1q<~(A%lA&rp|X(@lf=mFy2^|0wLv5U<_(Xs&I#ot!>0p z!Lrnbm2fo<{0*i8pQ3P^qel{n0*`DWkC!9;qz*)gX=e_VoGHKk{zG>9)vp1I5_i>h zqRwOPTroyuu4fMO4H$FF5DpPEw!RUF3gKw<--6Ey&N6HY<U&8ZbxBH&Gkm$K8Rzgve}fcoBtqKM!9EG4H! zFd2!*Rga$MC2HASR%#5CyOe1RK9o8nRf8gOTG#aKeAAD2s?O z0V0k>(KMj|`lIl9yWrf9@sCmx`c*TQkEiT{F#!Z9<5(Y&T&D?&8Ry=6o1Y)aC4Fxl zH6xkm>TqQ;e_e7=6O0>#KNc!pK_XWKBgfm}pP`0%%fIJThkAi`2n(k6V!g`VT^Hn80&(Eh!CzA8_P(#IPAaroD-rwTE-x3|eK$IJZbpSV-scyxcU z9)-oKyE;HeB7@J%qdiH6w(S!V+OTCyQ~Z{FBK|mlH(mS$tp#}NB3uF}#c32lU?Po% zMq!Q7w3HJw?o9<4HDrn9(cL47jOW{-@VBwQ|529Uqa!x3nNfUrcJ%Mh@|b_B>JRnI;5;Jd zQr_9B76je`kgEC;uRHo86GJHqp7$OKbq}%%1SX}hwk(N`8N@CZ1?yhpP@J`e?qwkZ zUV%7`kD+RjZ+_3Zs&rF3ncjRipmySiWnsblE5w!jiUh za|YDHe|wz+o-^pyzQ1Zh0@ z`}b=8x>elW+oQE!A<8r?I$B`sR-E7a{U#Cl2oiebU}1gZe@`R`B+<(t7VsdbS#M*a zKtx0Y+UH@E)!`p;yK>9B$H6{bc;(Z<07gwr4W+|f+5*rDlfLQp8t3wgivHdit~QjU zk12&n@G5heh*^d9s2{0$JzO1b4``Sza9#nmqGOLW`_}X)a=^0(bmvA0hL!MzagCC! zOdh9g^lR%7xr*bq0fpmSBM$TiSd~@_?`?(99ztULg@d2VdfvpPy!yP%0hH_|)+flr zA`si*-Cq9T82GsCjiMJXmjs-1a%$lyfm^6a!QKfJ3F9_#Z~(QeqE+7_`G99StZK8B8gp#eVMSTVn`&!-mt*)+u) zw4vOmM63f7-qJfcCZRM1-0_-)B767=7Xg=0#wo=5R3Gr3#Tp&&uaL%H&nlXGVb*qG z^%m3WYWcunh&@Q?0SFkcs$eP+=-I4eRFZdcRQv3)IgvofiFc`Y)$)ONf zPfKfnfL+kIAoKRWy2I63m`aHcK8Pa{d1_XWFyUQay?%XFib{V$9cqJzs9}Coc@1Px z;}yPDUs#iC-sFibhLfu8eW7p&>;7;5oJ1fly7_AwayyVIcfXh#@~x0z-2f#?ybbDh z92aCRMFQtishtn>lgFV{CVEovu{UmB6x4s|ZP#OnE5jEoFF!vXzJ;A#Gl)j{xlYzE zsE6(=9JqdI&Q2Nft#+&!g{#IPs$N(M2fNgG4%->N!dB}n!dw|4g7-a6a zzh~|E{oM-JFpCu`y*jL2oRqs@y#+2J>G0+7`Jpvm+kxsMIMW(hTN%8e?<)WZWH|T5 znid=%KsrG(l34{MHy<`^rey;UWKak34IaauMNR|!Y7ejtgd5*YN*dV11MLS$pp8&> zHQ?+eO&Kc=?3LutT>!*W05nk$K2-C*XR3Kkg!AviahLi&|3muthjN#_c=1FNN-8Qk z-HfS8%_TOoqkK%5l5`Bag-p=JtP;E&zBi(Eb@62k z-g04TBd7&p07R2^8^-_%ghMsf45|R%X?3tNjh<%{6rk|!H=|w{#|?ljZiNE{*URvu z(cPcC5H*m9s*(icAv7h#$LDCsO_bXF$UwwcDx^aoc83XfE=uLgxYaum(+gMmbanu? zf%8P~U4(Lw{CVJf$1!XYgDB(TXN<4KSPMkS7Q`b6`DCPtUz#r)nV=0sk?m8h7@4Q;`1Lgt!SDd&B1FnlOvK_DhX%~m_-kh; z*G2|Gq>WyH(i3fKJaMmN>gqxPb`%9B%w6NOv^z}}6rdLI-+~Z&bVJ#Cbv_V{q*b_H zer7X;`yMqsd6_65N)wB$8aINWtVKt02-8(Uq*qEkpq|#jA@Qe<Z}SR}UW?@3DaibHa-0yFvs0ZoJGx(ZbQ%u9GKGD50?+s5}%q0=No@ z*J=Cqr}_6|XvF{3B~>!$p+a-@j76*=P|o3@23o^hK^1jOE~;LWMxm@It5Ej-&ins<$ML<#@x8sp@Ao|SxUTa$hc%cj z*e4e7AeqR`z$#V;7)HNgLuL17_uszeRo{yPXC_O61t@Ox9|D>2kI)&yA3?E;RGvft%W%&w z{5pq{|1}a&DGH1Ff9W*a9edqAFSboU+Mz29+bv8s`yj?98mG6DTp_#>eFRv8!&nEx z3vw>cGGu=5-S1m78wX9##CO{H-Pk~YELb==w6DAh9{ajoLND}j=Lgem#WE$hzSWh-PE*KHYGqSZw*R^@qSMPxoiDsDAbqp+p zf`Q&H<4MVdyx62Ql`O;?r|R;a%DgpfBZjY~JF%BiFJ74v5B&gCf3U$Lc-XpicMK~R z@!}J3^q_hpk$c5cI!)j>pci4|o_Wy@$uYlT zdV*2THaJ=75|7qoB@|dNb|AGpXb}~lM{R;Gh|r_(;?qWjzxj;AD+g?}2XGe5&qJDF z6)qy&YsiK_MO@c#*uu`ew9H>G|FT%o-}!u1RBdw;N*3*+!{4}_e@YFamLcT2^NUEY z%JNxH{04BgSN>;*d<{HS$jJ_alZ7P*II9!JW8mv~*)Iv)0^0|t0G;dWUHO+fH9rNo zrJdXzt^Ih<0oUrk5%>OT5%Hg4dXiNgmsrG3+cSmpb_KJ-9*RxiZDG$@;#wdHx`1n1 zvD^%2P@ZbyAb?P`(d!QtTsdC>1Hz?W13fTC69`7(o*o%sCV2w z0pjC+n*U`&g-QclABhOzr8hn$2#%Ysv|o^$UkW*gZ3CMZI!LUnaHf$m8AK?Yur$Lz zd}JU#MKvmQNUr%TFEHNQIIKwGF*M&;X1WR6JtzBEG0-5+>-P3z$EEVbA45Ebu1`kZ z{Wj+L-~nU4n3M_^Fwc$I5bL5d7a4E0SZcz*hqCyq>^K!b4q}`~z9Enja%JHQeTkar z%3X(I0fpnPbMGdFw|@OvpPw7Nx=!SLSM2<}?lsRI0I=7Il{lV!%0&5bZM+w=rY!~Q z=%mKlcQXvuR)h{Ui5O?{rc_o2(u2K)c>!_Y!rgBm&C17Dhg$IO++0;xmwda{II$Q)OiWuC`#H@oMBQ zoF%iSei)q`RrwT=RuU2;>Lx&`;V}OR}xc?uS0X1ttH^>7G^w7o- zcD7qH4@GEV6xOGtgo&_k;?-|2^>ujtx?EVWZ_b(_|5X^v3;ur{@EKwd zfL%o(dHBYm&`j;!9QZ%KVO%>@C_rKB{roGJXi@{pQp^=B@sfQn>k`XCoHi~PJj~^# zEk_g3jbW`7@0wJGKtd1METeJ5MKi73qiC;35<7-hW5q4(VrJa}u$%6q-@FA+E3X zHYn&HDoLYUyGV=`P07pw_`uZk!frFve9l;N996h2w+RT~TS%E#ZopXXqxFOVCIps1 zKyYn6O+>|GuV13cDPnEDBUH-jVID1`rf+^eirs4LS4+^{-dtaBhI3A*^`9x9$GAjm zK!w!j|5oIck9i0LI7H)v);})$uyLlsGX*1Fi(0`L2Zx`H!JHz&2v;TgBz?H1Ax-pc zTehib$z=}&ct=D;kk1U)H*pAxre@A=r$v28i{N?#Cs$W(w`CLsKYsm6Su4b40e$3G z>k*PIR<4#DUbgLVyPe2`-BINeojZPvivIW^kd1*wSH1Q>KMrb1T9oX;|NZQkgQ?#K zw+2E;f(rW>LL<`Olr)9{@*{tP4BNVxY^4R<#xxyv?0Bu0mXxF^B+8PnwA$Q6+4L^Gi3HLK3Rg#$)KgN5&`ZrXmGIqs_&V>A-oAaPPL6-g&*s2r9DF;YCzcp2 z!14GCM-^`Gc_VjPT$TbqYtz(e7G_Rk#=uV0GUL6s*SC~4>(wiVE4#1CSvTIaiHTA2 z=q;APY)v~x{-i+9l{G>8TM-L;K2RBFKC6vBLGgb5zrm*7cjDDQ7&t8;%ZCC=o6=(7 zh}v_NmoE*$%*j5!mY6=NvP;I-lm#^<=1r0?Hk3BsA@t2NC~$QGs6sfH^l0^(V8*KS zSnT9Scp{bVDrJ}0FR^VYT5`U!!fCqy@N!w1go01(318ZHDom`;V9y+!vXrrL~w*5leqq-c=f9h z#JaeSwSTHbX`@^K<^79S8<&XwZG00+hc0P=+*2kWzbI|;@wv1^KteAqT2%F4_8e>y z2j!h^`7v^{b>{B=^=oJSGoGf-9F{G6N&N?We`SiuR{%4^<_TG_so_8NZPp9gW%Q*r z0$O?)`BDJKzH;RS3S360^t{biFexHnGpb7i{<*0sbDTLO;5<%HHgWn-og*B3cgb+M zFfwZIvn)%{+g|V``~Lf&^;?jDI#Ic?l0wpMzgt(HWo7+i;R@n(m^n;gODdXo|Q%LRCwdy)p|@1KUlqB27DU*1rAWxC(Iu} z8{2oG4D9k-BPcp>BThM0&C!yKQ>*&+iNc zl{LUIEShVhf+hnX)lbwqBDbF4qin@919%*vxlJRs7wf&eF-s6hrYq7^y7GXv(IpmL zTkePo7pj}A6EA^GBUJVaET(@z+?pmpMM!qQ*2wVXuW26h)_@e9&VQ}PHy9Ui>^OWl zWv7rN#XJ36(R8o51BvZFOo|mwkN1KpNqO+#I!IGK0`aebMg#XkCGcUiW{3s+!=W;W zQ3u8)RfNlG-3esr@qT+QI87jpVK_#U;}E|(h4xX>06mrERu4I1@b<+cou_@6!#K{| zZn2R@6Cv1emlyagIz36i7_!BZs|Mpk4IwHPg6d_TQ*5*M8xG4iyTOd#S0y_%zSabxWgSYcJaZ|4h76LT6^Afp#2t2dqSn;igVA zTpz1&!ra^dUQSJO_qME=_NB(%xHwQ5xjg@z&f0qa%vk<~rP&xqMQaRu(De&O(&?z3 zDe~M83~VCOYOJW)rnEMAa`um$`303aA*}-)Z-hAQzLa~m2;Pw0uWr|7_9IzK4ffrm zy>Y65wmaWF;E9o!l#~jOUYhGXV8?-z5pOh=hijMIRGn#S5QGL%B(Uge9SAa) zpHDEL@P=P{Jznv(<#|aEes<>Eq}&Hz83CC`Jdk!Q|4@fo&I$HNJ+CN-c=x~PNn`iF zv!UPO{+4rMXTngyoxo+m;SfR(5=lRa`&n8VyKh|`MhPcNl9P)3ZPk^HQ z)|PSIr?N7X1*LU$At-;5qP&6l?i;-13Oey2o! zs?WwoK`+_`gpb!oCTm%$=3kad*WLX1g?`O$YHC(~!F6CHxhk=%M z+@bSelT0;E+zSbbKzE^uQU;f9q>XfSo`d|^2wz`cAH_WJAmiM0t+6kG^ew5(Fqx4d z#0U&Oi~c@C2V8^ELeCjUf80@Hl$MO2wXu0|>B)YrUw$vgn^jcTF(U#Hc3l1In5t8_ z!~|NLA`I(X$Ehst-eXq;fo3FYfWXtkQOY@aLh}JTeoOr zvvsOt}X6S&KW5z ziwC_vYl{9w!%EvBb|-oW0Iu9134smthIsuz=_$g-DfA(PEAn+^rBp+YiIs1;pBgjY z&Wxk?xzf%Vy`7zv$~`T9?Bz+VB#UF8?WErT$pU=;%gaOj4us4o;t|NG58lrBA)Z70 zI5iitD`$it;}qOyHC%Zn@wPFlitT7ZJu}2+&{PS+_pyP{O@MYiz-_WRR;&o}4hBGb zr*<2LIlOlXnHc{3W}{l(r3XjP=2a(XMye^l6^)$l?5d5_PqttUtbZnQZ)zwcBt(nB zaTfF0IuOJx@zp0>+0Z(pHWP=?{2|4^c}eHFtnou~I1fv>41s27K2b@KyfbK_2w=8#Yn-&fO`|hs(C*UUnm-J@24sU~ zueqaY2&Z8J9CQR}HP+TuzLeAwU2oJ+Zx^6)c(i9v=Z7dNIhTJ&2PzeKB`8ks+xMlS zvPCahwYy+Hly#U=?Xzj#hQ~LEw|B`jpSEhn<0Uk9Ins-pFmUy%2c8g zjIw$PWl|rMIACr)27l;=o)1Qd2!EjfS041XAgh!Vi*>_2n;(e_S62pj6&IhInze#U z;3fsna%QCW?mWuG*3fWFL%aNdweBvHXBX!tTslAAyrX46W~4}DgC8(+FL7_twFMj(?BgTaof>*^<<2Q0_`ZWHZ zKl@4Axoy{55$KHIXzUM6p!oB&5FS5fW^!J$TRsJ>|CYz2SA2a$dTcEUV+E$x)DKSc zujs_Z#WGIKfb{+hw<*HlXoi0aZ|3Jmkc~86Gw!!Aa9KZNlxg@Ut=*0QnwZvLNWaqO z`b<$S%=l2O&$Y6t={{Ht1LNZ)yW0jln{;`MSYa~9h@Av%9vw1qoCC&+F;or5 z`mYhJteCE;fy*s&I*XssU-|e_J)48+vB`g6FXCP!G<6^l1}R;m?Tp{Q_jT^O{%`PO zZB5XnfB*>_7G%Q#7`sdfUtU&3)1rO4i04lX=Fl+$caGJhXJsWu-T*UjSgWV6TO`mT zv>$7b;xJN9ds<`4_Y8+Snb_d91+IX#FQMA`Ryl*z~&^Wn}`YpQG)nm;jxS|tYZ#X zS-P;|`L_Fmi?qo4fzQv=P8Tb@G@^bgJC%N9sA*wX|YV`cGUg{~aE)o2PrTg31OqFp-ePH&fDa!&AczaLk!h+JOz9Kq zBr_6Sz!ALzP&i^|D3TlZnOE{-zA&Tm^-TX7B>voGcdXoy{KSPVK7?v;(Pj)_944s< z6Ue?_UyP;=fg?D_g`qjV&|NU&I^0Z55uI_EgZK+XDFGYbcIY&xN`$Pq>|0||IvSY2 zz9?|L`P%&BS8fIl3P(rXr%zp8UUk%r+KuoP8l2@n0T#k(*$)$N1B}k3a~}wl0j(#b zD!g(cL5HRo-{DZ~#&15ip+Z0<6Jf`xDSdV@AgMkxCKL|Us1dzCgqvIZ&BWj_y4!aS^SUgp z!C~n0s|$ybtLO`A9Wid0q8+lg!lp*GK3FswOmrqB$6Vx+%|6RoHP9PoKG5OJAMnJr}rI`I##jm4>hXUKjt4wl2K)WorHbhQ9g3XI`+asZ)fza!J1{5L>NkX`t}@Qk=?`=iSgq#3rEu)6 z^F>V-YU;DXi2@co;s}Io0Hu=%Is1**u*Wn#Am`(Arjg=i93d zPjg0wPtf5SE4mRg`MdM=ja7czZ;$-fF-a)cpVB=Zdv~YlAuQ3TUuVD-0w`ep;&k>F z^!w(h6Hukx#?|NTChk2F`i6GdpA6}Uss>}--i%^gzQDpYw+mKOR3Hd#Gghi91nKcw zTU~#YAN$qlKQ%Nk(o?h>BcVhr`ZTu8)QV3L!kx>@a^_P%57UMzDIJh6i#gbw%8Z0X zCKP2-(g2q%zzPG)9`OLlF(n1UETvue1~5OL>}$G`a6xVVleaa*_PU1Fo{<~Q?Y zXL2kO>iro9(~Tm2XJviIT6DIhe}^{$>VPC-7{bsowdPnK2is$lL(lVN~rk~u0a9n@*SvNyKbM;0q zxuD!BF{{Ngc0LmXKIQ-2A0@zx@sOuRs!)b60 zg%;-GU$$y#YW~2fu&7W+ffCppD|iE6O2$6eaG|p*T6@;naf_(h zs8Wrhrqt3fOUu~J4zfy62&4DY{eHh1*7HeFWW;YnDtdckOq-Z7$1%;s*;f}1fL(#2 zxaFJ|*!r44h)FO4nDdrpx7V9?XEkvjmhadsn)DIZn>5|cLvP@7XFdwdTT(yQCEwqwZR5RE8d5&bi({c-4}&h1^!rJpi@-? ziWiiSm@LY!`g$uOqIF%n&r^0~DIgbws;!~D0b{0Z0GB`2$JRspWP#BT@k4q5n=i`hA5REa(VWScV>Plm(RNIUnTv@> zz-M}7NA~PE=fp^>C9lSB$C99D3L?Te*4+7?Gy2{WpjBDor@R4i_Op#NX)1MfbtisG zOy?pGQprY4WA`y-V~{QMD4ec-U7;QX%xn8)W@aW5WmhfkIv7dekKV+4xCG_A3Nw!o zu2W?BQy%osYodp(z^7#XQkUSjow{vJH``a1LS#%2J^0^;p)6CAB^yejD55l-zE~fR)n6f&Nagr(!VhY@ahk-Av$zSCX(c7k;w{E28Jg9MNKC7*K`#nB7J3EJC z{#uK1E@Ah2Hf3Xf)Z$>D8R*Wjz7JO`VI3858JSZ!D?y*z`w-HVpnLZo5rZp)SI?jH z^7ei~LGng1iuJMIjKzsyLRM;OL4%!Zr$e`hNiNL>A-?5yd7RIqecQC;-Pj!7 zJF^*=+z5V_!o2m+LzJd<_#7?2z22BbKl@dW1Wufi!q`3YUwbl5MKMI&I`|Pq)4a)) z6}CAQCVbZ`2Q!0ogSwT7sE=2ZZ-Pb`0Vzer_j5v4 zR-&*|IEAu8zx@NS1~`3<41I*nkvKObwIf|8lMF?i?7p3PyY1`;SJLMPK9d(}U7F<# zefkt36wV$Q>jRr1zY>iX&I)zpl{Eg+^ zyrDt1k7313sc;4NO?i)0N}YBc*I z&H_F<;DinMm3;@$TX1A1vF4KQGdnmJG+q3xQ9e@$ptcMQ@qv^|SepdI6`{xVXr9 zOuxXGyc%zh1a?krpv3O~2^^pWc|(**XC%QfHB7EmS<0PEGoe#g1HWxIN>-2OuVC;Z z3jezt_snO`t{if1 znu$!C!)p}1G*zF z7aPmFk! zPu7J0u&@^19Kso~E36LC5Z2P=sb4kz7-g6+e#vZA6#xCl@_f|Z0=JLf&n2BdXq0uT z&dAH_@u3U1@U|9XypL;ZKZH>47V;mN|U5ZJ#w&-^4_A{%Nd&yIT~WT=*2=G#Sr`dKV#ujJWpEFQDciOd-e3XCaya z%Xp`RL>Ss1r=feoj9jH*+ryMfwnG4!imPPp$Ef#XR0sk1H_0O#W4Z8y&?yGtd~3## zA3=mMRz;x7yaq%^)C91qf8qvB+-Dmw!LF^IyLxG5r1jFhyX7t)sWwoyR%^rpE@Ton zbsii&xzl|j*1bx~!q+HAgI_vu``^D{hcbUt@SqP2O7IO~jH03@p7f{;JO?_|ZiAPA zo)G@AAA5VZ9z4i-bz-Nan`r&hp?@Y{hf4x9lT{Cu${ts{hL(;{2+)*(*(uQQN*)J7 zJcj3R>j*{HnqoU;jmV0dH=p=Yh!q!yJ*2sM`Fs4_U7iEfbUi(SltJ&EKg(I`{sS28 zu)1tv_dR;>i$a`2!fae8&z}1FP0K5D)2l02vu@uGd*!w}wmtnsTm4<%s{1v4c=-g24-P(_fmm|cCjM7O*ajOIJ^ra(L9+%P>3q-Eu@Y4@$|;A+?bFaYcjmYu{?f&Zs@fYAuG6ciphy(Uv2L`;np0F&c zGDow|n|GN?H6=xtEKGfx9w~%}@kZvYQl~Og0@6h6-Tx%Mi`g8apxDW_<=%y;y^j1( zPh|+fI2t`2R0ALz%pni;RJLy7G4?BN-Ag_93>elRnkd-@IxIZ{gQvO1{Sty?U>bmw zMq|J}Rk-cQi%D;0B8+JqdgLI?Jko;`GS#-V^7>p1h%zrbio6uR-B{&u>63i5X%ny} z1qqfpgVX~;s!$(o$};i;Ym<3Rq3`N6NV<7zZRns3;+a4TBt(7)ok8A}k#S%Eg$VnT7e8e#A!<%ZB|+66)n$iQ zI}%g@S5b&(pGA($J=7mi*Q-Mj^f0jpYVqnQ@!o?2m2ML~F`#p{ydBxBJhZ)|rt|T7 zanm=HHWY(&Z@I-UdW>;dva@&o8rQvL9J*ATlJ-j z7kp~>f)56pU#(}hZb&%1fr{wP@%GNZ<_Wi5+|2gJgK}nAedy<4jBya;Z3t5v3O$U& zeWnHxo#g2jRD{* zpEZ+o!}V3=;#jz$Ny!ke=bkbmm!${5-4RoPBh5`zr$i%FC;! z1P(+)>v`d+7++pz&b|a?%5hJxR=zWd`kxQlpK|x|)NgLQx4tR%#GC6L0ypNt9i3j$ zh@}>dWB_G}JY#5W;eSa}b&qzRS6;yG(;Mbjm#Ef$3-}BW+(qr*MwmIQT>YZx{(F;^ z6+FUU!EPt(d^h3~Eg3>JuU$I;A4Jn#N!+iG-wJ#r~)0#LLT z_dEl!zt>ciA&daA6vTu{BBsKfwrDq=ke=EiIB^SrHz>aS}bg`UkeanhsNx-H|q zT;9iz9bFyDS?3nzlnps9Jb+_f$c{aG@1;69@oQ-dZkeavMf00Nz}FeEniWm=fe~K7 z`3{9%$?`u&5P7`Eu7T211t$y*ZS9M2gL2#uuxgqLGBrTn#Uh$g>^MZ6RDQaHz|+~; z?Dv>R2FK63v+8z{oUsz`Q&5rqNSv@7k`_H`(Jg(8#wX%db=!a8qKvtAspJeO5Y$14B)G+=> z=GkQ%hX(1xhZ7%(J@?4JJl*l`Z{`5tH$|Y^zrLDIKy9`k75Pe|eNq@NXhpYR*su#A zm8Ur6E7+B&9kJV_v}Laejh2=R5_s+b0>iYO1Zj+q#|rEz{ni0I7&nZ0uxw@V&YgSq zNhY{lSEwuvSey}YntqW50)@S<-*7@Of{5a_5oZ$T+b8|0tKaAydx5}w<_A%+v(lyv*lTF$ z0V!4)@1D9bmS+uk3^a(U(@M-BRZn*+^{8VoM;Wca!^WnZdCF;|MF@T}6v|+*W4O~k z6sjw;E;uM?!6$5}Ro@=(u9=i=O^n*U zeLBNr^I=(8$Zy&tv3FOXpFjt-qO3z%X=OkbBUhnQ zAqvHTuKjb_H<+&5k#1VR!)x5T8ur;}UI9$D0usq_8j$x^bH$7VJNq3>4on&*@PKjm zwtQdJ($u6H8umG{W#-!K$!UYxd zIyJa&a9(1u$&k6B*x9aDIQIf0gBZHdle28RyE)MxX01Wqy0}Yt}DlaV!4KY3g`SLk8J-pkoVs%PF>-O*CFDj zE&X%7j|=TM*{^#$%}$!7-C5@lu^adN1J+GEPDalubDy}-(HTn0TB`+YcyagcuBd~u z_SDooVGqj(!Ia{cCw2-s?tko_NZ&ks=QbrZW}$~C+`NnZ<;eIJBf1_fiajS1X@1t8 zyzphuRVnMd$_mSpL$Nlel+Gre};$&RDLg)YK0>kfZ;*_PX^S_4w3ZP=pBuOyr)!A=f}1T1F?BLuqSlmvkGaI6?t7*kw2=a$g@tH zs+OwCef)X`Mf6>%b*r1fNZmS!U!s5u3wInvL7Otp>9#1oO`EWJKZXlwky9$3F^ivz zs}e#0U3@{0>rgNYwb6cEs}a`b|r@6Nl4FJiyJ&NsonuO;V~#u&K8s3L=L==A^i zL1Y60Wr3_are!VOe7=k&OEOh_{!S zvc`(Z0NOKM2A%J{aHocwhaBp(Gvd2IJ^;Bl!{_-|yIfFi1{$V%4ibeZg<-l^T( zH)^Ns&yZ#i=bb@DsBeO_LO|F;R!miA4mSnKlLtiYN!bjma112@gibdS?n@;+L)P@o z%*_vD8bK6#P!u4aX?o3ca&SFkLjzAs)@G}ZA312)CVIcFtacZSY?+t6T$*qgIzKyB zHmyguKRvL#v`fH4jhF=r5gakIKq0@vRkm#5!3x)$h{J`MUeGitfJ7OU}Me z_4RjAOUZhUd3t&}<7DKJxdH<$&u>{!jIeCptONYUWS%~Vj$1wF^oL;x#hWh6-EnuiA# zYFW6@TK+-(jP}JpRdF{KMIt{sJ=$|;xVMBkE9(M8_<^|PuD6ZNo2Ec*18LshP%1NY z_YgIZnW)Iy+h9OhV3>njo}zjEUz9dqqMk-a<6t_9mI`4>62_Taj*gDdf>)rlBbEpF z4!{f|G502(!$k3fF+Pt(yf_%eYWpwit{w7*(|14W&OrLmqoIiy%77{y>EO>emeWi@ zN5fV#Ju?Mbtkw@#5sea4+0%0~a`)Hk^>y>@Sf(n_sKDbRW%Uf^U4$zFQ3i%@ z2n>55ko!~gy#a^`{VW$g=>VP(NvnYs7MTj;t(@L#VliJAwP!u^Q2vgi zuk_M3SzNr;)v%Fi3vxBx#A*E*cL-^QO-^pV)Dd6Dy0PH=t?LVcZ{G@@j@W5-A;HUo zo+$vyMZ6qp%&cE1yWdz_*Ehz*Ew+dfqg8&e#gKnvIa{Q^!Jpws zJX=_(prP3UcIv318@Mk`xU$IVC)uCHTm|#R9%4o`W{;_zI4ghs9Q#UxBrW$JYFRzS4a#Pm&E|Dno=>A$^pfb) z(@$=f=Gei_4OWqbYQP4Ii7w8Q*(Q4LQ4j(zi9Lm^IJrhVw9jbkd^~cCb#bjgadG+y z70kT^!O(+T>Tg=TCz*=@?&Xnwp#%$o4gAqkYQfMa?E4+jI9T1$5e+5UgZ=iWq^ujY zQP2>nB5DYdCnKGH=D~W*x|Li48kM(6I5qGVP7#O*F(VFfS_ zvLz#oFEnzlV~xP{U&D{CY_xred?@ThOx)PGxDY$A3C%W4?9VH$BV%(guFfFN6!$Pd zO~Ql6;Ikaa2lP(nWi&4g);)_!K+On#fSkV`H%@g7Iz{_2*_n-5Q0HucPcIYX8HP@L z*+_f^E;9nU`ja6Ckls)bV1)O|KI+L9#RBKiHY2s$P#8kyPW00dcw-_2?lT#!qI3F^ zP3$0Wx_^#Wp?-_ld+)VFGwRWlg%o=&>osx@c~?8F$fx_QbCbKtD{JM1V-V5VsT&s= zk-#D1F38ct3;`>Coc`wfkYk~*0^L%kft#dh=Xfujvis6zWfwL9CX7-F$ zRxFzv{I#O0|1IV=oS;=iCxyaWGMTv$nP zPDcs|k=#MthTDgkg7$c9(nzeLAOWb=UuhF%ia4d9Xup+|_rB}fN5AZ7ht=mAW= z658y$V3H$29Niyvvm7gG68~7m6?|lPW&;P<6?voy?&d!#48BdC2O65OvGs2n;&)3c zDunzgu7L|dd=rs=)O!tIeGUoF{t*#j+x;F@gk+ETd%=>37%+nJ4xSz%@Bjv-#@R_= zPMkW}6(pzaQg88Q9A{{6Eb2F)33bo0HQ8{8GenV`Hjflr>YfRXI~VWZ3hsfaLo17JJS`d(=fuAt-MjEL%}7 zDE%odaRTG;9_G0{hnqS(A7gPCz=BdXmV}~qzDede_$6Y$y>#J?OiM_*31Z_5y#$H9 z1%^dDC2+IjA?<*V5Ls{n0s>du+;mv?mZJ~`CIm5NC;&MSMo|e1sE35f65vGBR=VO}U-&N~g(8&70( z*7O&zvin)U_$cA(tls<;L($09F1uV~{L~5p3-a!0fg}m?@P*YS8$(esWD$ZNm%Z3~ z4KrL)yP-K3HGDzxG4Nao?*m*->@|oTz8-xV1aYpIQ+jd|5ZT32f+v}E}ZFlZ&6z}8> zfhAX8SnQ+(BfVZj_M2(G9;YP-MgtpKpdY zM5f8S-9^fB~UJk3C{?c=48rg zcvrY&%;~7!Ec*c8v_b4tFcyfS1#r>$!z~4lW)QcMPe|=YH&}Zz3~Q~6dQGu;9y0Ff zyxX=#^mghUJZxo;!dUL99UYKx+%kiK%g@rAWb_DU2B-`hK;Ca@n~%fCC@L%1m8A1; z{4U6Vprk%W8;Y)aCUb}sQxU>VW{$mdp(!i|Da%>htIFJ>+L@;!U}uRQ9M{tkwO2R? z1O%wyx8ptSYvB&qw_4GwgNx_b*JBa8_}D*XH^mL;XK)RPo>U!e|1>pYj}IAeDnrNV z^1nHe>ldzPC4FL;sKRWPmPPqxrdUO5>y+QO16RkNO;7YHRlLT&Ky)0+S|L$UO;nM) zaXhj?C0VjEdp5m%5+61aox`Y-I0D4hh!7Gl5F2iGu~;`+Vtd5`5o*TA$1{(AxK#~W zVMR;JIf@f$Ves0yafJ_41&ld`aD6SB6XgKNvs(K4;qXo|gkzp4upNO!EtWM@%N!2G zHV$2VRvaI23PM~Nu7it?5C(gkxyC}i9i5z!09(fD*xA^or*!*2p`r^zT{bq6eT#PL z*riLq*X1MIZ4XgE2AgBCalVAt_hWY1Q;AlRxt(5EBeVyH_l&te%Z_6*Hq}e+Yyj<;+07!?&HN-yVVP zOI$4RI`{uW2qi2i|G`uDn5(O6une4zEKsa0HlZKTdYuQuPt6$lwYcnFpI%L(g!>8g zyfYvSJh`$|b=I1aZSb@q+Npuck1I>q91S2PD!q#VN`S9?%g6b9aS5T}VIFh@*CT3X z;wlsQMR@ym;-Q^N&j-niJ2Ju64@>KSq+8nE?RWlz+4lqIpM6ZNS-9EkaigoFMCq+9^@ppDYke}f7BPP~#>%|Mh%bu&%5qzp0sij}l(#T$=`pMsKt4+q zJjmkX_+YsJ`Hhw->|AW2$x*Jlck3zN{DNXz)gx-`Wgqi-2LF5)@Rh!Q*Uj;g{oT!Y zf6IaskWJ>x$Q8bToi-Ut`RhKaGW4dHfLy@ynf~)lR{cHp2NulM(=7USEDZ?mzISh@ z%q*vMld2!xF)%wP{g%?gWFU7-a#?9Qj26+a6>MHwoUzBrM`~0Au9*ZsONg)IB7)5E5*iDgJQb=K#rrV%`ulhN zU}NUfbtx&LKUSq_)}1eP#=yW@@S7&>y0KMCtT{yIt*ZP1)S^UAy#;7*|LRP_K~4M#+g>=iGbD6 z;B7+7faGvlAx(vKXxn8t_MY1`@1twv?^B5;5}sn)Eb9W$g_S0%Pj$ed1(6s^MnTpT|KLUfzQ!b;84!cUw%@XT$u{hGB=Y4t@&1g17xL{ov;B$2dn zVpN1mcH-?uI&yy@k;DRblE@D*ZYwZ3$&)RnCzdt1n3E42RYM}36{s{2If91+<*M|S zE#n;>!&JTyn4d6RLt{;-E~e*09N`Bs)cV@otM-wOz*!l4tA@o;puE&jp?Cnyd3@2^ zhaEKzu_(n^jmcaHhrP;`D-!U71o|TDpQGp8i9^BQfS>Pp@-&VnWGyD1${=PBmyEo+ z^5-bxFLkzfE}1VpZAdGN^3!~1Iurs2w-AiqrLIky0a&3O=4t`PMbwxmdEsQ9jJNGr zzlo}#nV&Fz`D8DnMG%6kVju?4f2T(b@PwZUXh;mdl?QvR4_E-QWB)=Kq?D`h7aiyw zQ1rra^r_uOG#U6s6;R9{a2w?Sd-JQ(F2ZQXvEOTdL0{u3niIIbe#99-u*LY+E|Rc? zd!LNw(GsemlQKfgX_sC+1tz`3 zMG{yAW=O%He_$9!xU+<050uCS4Ho$TXb5JuYk`MDEQg>7v2{DvuZU_N9cm0NI7t|9 z;RNOlrGsn$oFM_I$P#Q-2LT)L!pDS<;LfLf@R)p}Mdt%4b0G$9} zCvyza;8TXbjIcj8Y*^xZ17wMaQ4zUO0o(%1DhF~u++epbt&abu1NP2|^z(H7)3A8E zag*RcsK+qN%;KiVIGJ$-zAXe)13tV17(FZ>*wDz}M?$W4S(JMTS^#41iK`<%=T8Wl zU7W?>6p6RbWf|oJ5W60DL}QGEQXm<75(nWGxKKknP>NW+=~-pGuS5tgp_5NK(}y7F z1bUyfa1c9NQEYR_T*cC~#HEFJ)f_nEq4maCWCu9hfL|fk)q;2_JUtUr8)CUh;y^Ks zg-s4&0mJoZd*tNM(9ltU5s(*sYH#O6_YPYeZ8Yw1=OtFM#2O6z)sZIn^KLnTIM+`$*2rVA*)+8ECEZm>y^a((PFR6ALYegT_4MVEa zSZ`3Cr5(C`7tU<>1w{8@`y`l@)o@AG&F;s~3^xh;hBt~qIbr;JX7>`xO}z3=4w?9x zU1hx`#MTo;wxsGgG$T%E?C#?sdNa zkIg8tkh2@1Ie!pljhL>Mp=$#)f|BPLYAkY8LnVeQ;{=3L;C*H(k%fgb7SPKyRUZm@ z{hv=x&r9inWcd)XfMkg-9V~b$LPP-4wahas#0`WF83W1Ctv~>G1RRZ-kZ~;8P0$EH z9C)d_00Fl2>8G=^xViT8ZUl<_1J419W`d27G)M9K!NC{t1Bl!KCpqw2*q!jtyD(Po zI7#K?f8Q@K=~|$%kr4rRL7(o(bKs+ZBk~~59w=cVNO-ucW0t9}JwPq&c8}F%IT*Am z_^dvI(r~0sg}avQED$A|5ne{dwPis=5H-Vdl-LTv_uXIyTmL)28Pzq67V2=){4_TW zb_|E8ig3p;)XY>$(NDAa{!S7H=r8<^bA{JfadSie1ml)+>?ioX5C3@&gH*f|EBF)O z3BvOiZOsPkoR)kZuu|X|gEk+9{vKS4px5#a5B1;>?|gNE8m})Id_mjG6MVe9khcY5 zyn<`haf9K@$qNAGQ%0&IK+yt#cFfFd8*Wm>A3lR?(U^Z8R_KXT-OVeWhcG%sq=X72 z1jxptLVh0Zp^<@t+BgqClzEl7Ux>>qz@0Y62y_>Vt~UXp4dEfCEgHhA1^``3$r@02 zgU8$m93>>3+7CTX0EO0KJPzixHP$FD`OgW5*8yGehhzrV5oz%;`v3m@9^sWZ0I-npp!Igj~1cii{Ljdq>hwFX{ikS9eYAS|g zzp9%evc4WPY2q)7rlq!FmGzkJy4kB|b($?h$(I1KNenH}h@chSU0+|{ffQl15aigx zzXP&x7{88ke*8o2?_M7Y7*)dA7}G*zXcXe?kpR@h!J`O0^?wiliF5)s=S24eF!-PS zYY#}QM@7ZY(4WYoj0ZYIP$@J*gvf&xQH~`_yqSSck+^E2f?D(W-R*YfVduBfzY`nI zQ;7x#GX(nwcGzFN@Up0~t2>6TV6?#9Z1a(0$~^eE+-;#dtRA>S#2R*h_WHOcok#+*-b^2M$`A3wkWo@rHy|VlH6|4hmEI_z9 zlD-}*786@xX=!PL0pExSc1p_oFnvNf!GN8qjXqfp#3M`wmFZQ0vuPYZPKHyDFB9@= zBbv1lD3{!w&$vf~OF1)Yu2IT8ym7>n8KrGa`27(b*rf=8jD{0ow;EClG>#o(#ht{4 z$I9~FWp_o^`9RT=EVK+~f^fhEg4k-))cwxsIeZ`64&4BQI2wuC(DHje8uBpE&A$5w z2Q4Rt1dCyInvQUY6aW7Gd(T+M^t9`1_;g63{v#r&?YCLLYrF`tD-{d7u)vK4^@vn^ z*iT=C4U~YKoDuzYHN?uFftAgyv8}CbSwn*kFv&v(hs3u_zZb;H$x&ECfnJU)*55d@ z@!@XW#a?fXy+x>Qg0V`1a5@IHwHd-Fa)*p#&mJV?ZWA?B{@oM$>WzLN9(NELLllrE zHcc^@3#454!y@H|3m^w-n7V2Hnwpx&hs1eSW0yWd3h=@(ojEgonvO8%VWfN`+Z;XS zZMfi{d3Pn@+UoLq_>`5kwi=)*G1p0N3Q0UsY9 zxsJIVQj(H5Z~KexwSWHH543OyW@kWb{m_W%A*&hJHHA&LagO%bU7jbGAH%#5-Wb|I ze8I+1WZ$VxAw6;yo))@zKslT19zURdiKFmw-d8lfSQCA}e?PN~_MI+)0CNW>0Y%1N zwIW3cMJMP(UvH5$;TvCL+Aweu&e!7DZs|a zayoWrRYS+0%MO*Ozd1y1JC*f*97A8Pe*4S_|FRP>%rm4%h{CMtiJk{cEL z_@A-l0bFfmXkt)PpGbeWgHPrhOmiFVuf22XW#6)eOsEhupiz1f1jXzsh5d+*`KX1I~;kQY%%y(=Q?6!lQE;*qY$l{g{|&!oJECIenXD zcx2=P#`G9g;uw+#Fd>BdgFZoPb$KKy2`q-6D7bd)+!-)DY(uVQoO6#4d6U1FjE3;7 zUHfS0NPyI3IzD(-*m^;L02^3xpG}<0)2)@0+NjO+4!U>jshyINJm=4E#y`RibjV$T zBidWj-&lcUtlQ-cw}JmDl8=M&`_{A`=q>?D0a}342CHFO`~e9oLdP+QJ`I0}H&KDQ zamUf`n17KikFZn1djY+f5pJvmC;+>>g-QNPdwVhxq;TE?3oFALlZxav!zS4SjP3jT zDd)aY%E#*_H-|?=z)5urN;g{4u|xEBn(ta_QWDskckz;7#I+q)YS%HIf`S4Ls0-mz z6U-%+zWJ{B?LHuLD8pz0RRDwZ2TO)n`s1Mi!DdHaj{o@%xcGONKbYIt+l*g;87n=q($Y!zvUvRlD2)Eh9W^%QB|R8?I`It%u7jsmjba-j zeG+R8j*Qvm%l9yDTF3OeqPh7tbQk(4voYPq5bz+1LelGyRy#H}tB8lR{dhU}SFZpG z>{+13^;(OBKeRs9Rrg{Z)>Jczd7rSLh^97vd!@f3gq>gEMauE5Ft>UV#J;99l9imC zEK~gr)d||lhFLAc%u@p+BQ+?JQStr6Rq!=$8!0ppD}xPEgKi!d8oY&{;&z?t`>1G~ zduyPI69y^%IPyU{K!hke{&8c=VpMD_@h>I~S3m$bDJnRn5I6`UI7-OCAb8OksJyb2 z$0Ve6s;q8oG}W7RZGd~d1v(R5ysNiLEI3$_aTI}fLwE4tLEyl!m;k2fXZT`UVCc!oAO4hDQCY;m z7*6sKlthcfSF><&1pw(JYYQhJK&hi2o;^fE7RuZHsd^WdS^M{bspJa-Mm1f1;2 zz|mlGrw8f|nBP_DC+lpx3>*FqFrf3RKqs!$5Hc4TEp5wY|D}HDpCzZ(BV@m6a_v3$ za%GE*q~zz-Ehyd%-r9WDX3t$62QW^2XE$%&jHc=cwk1Ww)p^qR$-C|Xj#-Lp_y3U| zC?IXVY{)V_ZFW%~+6gY=cnQ-vXviGiEYD)ljBg4`;vDhtN0a^>h2!w(sD8Sk2nPc# z9$w-_bJ$DG>MnSQBH^un1^U+X*~xN3}VVo18yBC`7xxh!re{8RREvk_>xt%(^UeD)aT-SYF&w=xz2h_H*lFPL`@sX29O8v~xK{hlO5b}`>ADj;%K$y^g zHu{e7^6)&U*lEGpfO86qrT*iQs&_He=H?rPv~4a9y36Yb4KC{*464y=+ATnfW;zq^ z2WPQc6bcW5Jlt3EiNM1lM-kLXpd$WX<}1ms6PfG+ih_XcI%MvkWykou9G=97CTni; zg#}Ue6`f4jsEW#(jK9Mec>zU5ZX|Z==PCyRic&>E_YM{a0xMwW1c#Q7{Ir<+r7X4k zh$lvbaZ}2#YNYbZAD&Ts`~4j`Wbj0A$hfJAq3tBL0bpBg=#~)#cbV-93B14z)!9FP zkeuukcgYR6GGdjkVAF!FUyF0S7BE!nhYyh4g^sn;#fV;h`0xrmPS{#E0it1F4Wotu zl*J$>mgT-0hkBq1S2K=Gz$pX?0${5j#&WXwRLE0nx}>C0wF&0AjjcQK9?h>!Pj^*i zN9~CN$`}QJe(y5|maTSxS&3>2B___hLvV}WKl1P19fwy3vN~K}VH*Dn>QGr=21v)q z%g-Oc!z8E$kS|BKoc>SJmveILhTd|5D6~I*oIqC)u5^kruP&Zz>vOtvz|_QK4@zJx zMN?LlFi|ad*;P;UR5$G7n$_90T zb^@aKCiJM;=7vZXy8@U1nHfUp8uWpN0)7%koF(4Z$@TPsbCB z8~t`o36q!y0g*1reI<>}$&(VOf{^?q1N;BUkHMwqe8i2bZzCn`7LY0&QCdi`{9DRD z{j5}1fEJ%94PD1kOUr@bVJ&oRdjePXA}}%y{4PO}fAxM_xExL`o&&TPeA3-_4NyBV^;njK-eMD3SrMhBQ)wzT-}l}=*{3-#edC4= zf+Q5~%GIj{z4)+_Tes?7W8q=b;D2>a8#C27LFurghUCC+*ogJF0pVfYh3iljJ78Rk zASTl@rs{kng%R}G7-+FvgBwE=T^orV*G$^$T>Z8D1!%IbTN-fw?qb0_dGw@WASZ>} z+S|2Go_zeh>sj(6JX2sqmr)34U?i?B8uUBc93=tB@}uQZTNwW%uWMki5!BHDP+0sG zm(R~kvTIIPEQ?asZ@K>B%)Xdv5>MfPlZZr14FFFkP(P4vlZ!WSsJON7!G$7?giSyJ zz}Gm*KX^qXl(xD~wBmW896N{)e>cVDrKK8hGPNiwwa%Y^!KM+8zj_@FPdLi%6Q;oc z`HV5=L!;{&=E1;j*AlwW&>cyIRMvISLR=gZFvAq7#o1BG#rKmg#bPo4#_S?E14QY z8Rnsz@)J>Vgt7S#cfc9CMj#ma5l(cp?0Dkr&>OOAFd}4ryWi}bF*bqK?DuKM;;}2~ zPf)rP1vY#J%C{cH!zK|ui4eB%m)`4@lyr$4nuz+y%r#FO>O@yUz?9&*)r#dv4qH$u z-FM)9y0HN)P#7@WLJXj}_cp%81ZgeTfNqxOMM4OJe)hkQg&!Y*G6O537GF;bhgxmA zPE6s624MUAc(6i%goPn|Q61PW;;)}iH^q#?!xm(W=IV;X$7fwY-fjXRR_SqUZ)=;w z7(Xu6OD}v?-lAM));)YQOcrX6-K%!digIm8vH5f^5mggWnUbXO!9d|c~&?=l_^k}&{dxDI30KFl7 zY0p;0w5!*2vUlYAa0j!2SSHT{di*u6asP5G3Pe#s;69Uv00SUBhAqyM=|R}j1foW= z<8kGs(Mj+KRoE~XTnOTxr{fV-$*FB8dWwa5iYMm2`^2lRyO%!3Tx?gRksvpp^gwwA zRKGWLib)fmxKi;$kW*~uF*S-l8SN54DvVFjC(0_!9l43B+WX%xYtqzVegh1PAb}BW60%h4Guo1apbFFCd5 z%NMh~ejIOsa;W-tW>kDG{l`B)V?NSfMNI}c-MDNC-66PT2h@Juo)=-J765Pq_C*Tm zjX+g#lJOFUI1acs;OT(Z!?NVTi}4B{z3|!avFkx1T$mNVH9ym7RVINslfz)F_{zSv zt)lwn!WHMT8sNsn@#uY$3d7g)Kgleojz_n5&%u~A1g0Ap-c@h{NE(BT@H#;bpz)~r z{FxhfnZAL61!g%ZH<+O9LyRm;_Gu>%)7jWa`94X&3%T^wImSR<1zRS89#B8tu zHk(rP{Tfkgv1!EAF5^k20ZruC=&c zYjDo@jlaZZm7sjcVome&$&gY|*ofE9SEA9z0OVy8K69t1b#IB`RAY#@; zd8Os&_v%I9YB?98nS49LafDxoF}|Yf2M^8})F#)xhVMAcooQ;N{XDVi1m=$&X@naz zT)$>P5fyQnhJnm7J`shu&oJLPLy%gq=Da%k>TEVSV9^D9d@4M<3%`Hk!h)`Jz}_@h zV;(N-dVr&eke==Q#g0?OUX!>n2LtYe; zB&iHXB!!T|SON&;`}xnW85;qMbN_CS_n3Z^dk_0tEGKz3fWVNbNRVtuq;-uCl;iR6 zOaX~{1U$vRA@4e+sqZL=w?oOQ20$_{fS&~*N|HR$^_I`fgV$=t{B`sc^VhEB)%Pxk z>rdZ$1A+|rumC6tO+&*d0MZybEn2?1auKy-`h$(HaPFWr3_{`C)r0)a!cTmY0Xe;gAx z{3s|%HUf2dAGedhC5TNQz(c@pK%f@Jx^Blcdv8Ny1({n4Z8k%q!f2}Nhm-D~1xo{0 z^^U_>CK7Ci`w(z8uK+C;tp+|nSOmCH9X)=32df#Kkf4jUh^mQi6)4W0fTb6{^Tjx< zFhYx%W4_M+=ZgIPfgR5W8@v3UWrFU)S)#=P-;EH|N@%@tSx7;dp0L|P7h|O`Pz>k| zc>AZzRN)a3f-uDki-;IQo<=0VRxhM*gE>S0HTmZ?!YT(o7sP+5nA8SXr)s}0;~WMS zJCItFPmLEA9b66uKnl2|9cVFO9!QvP(8z&S$kUW#6a)mQ5Wb6roj$R$iV>TncRE zULPWTe8_`f0lB2?DEOu5SanP3N6}|-OW6ha>VLw1h6v!7ZG*-d54R~YAd`&@NgO%) zAe}V^amYYZ$kIS@DTGTDhlvsJD9}!*D?!EKM%}Ng%Y@3P9 z!h(a1@Xa9x!HS0Jgb8-B@^aO&v9a!_{6I1RrxgWGk$I?YT{qFcj~wO6D8W&{U3AhP zQe4rF9P>md{HGuv137tjkEcUsdJ{g%Lx63tqjP|eWZJajkw+>*LCI_?$cjC|nCS0R zs90T5M!Ss|#~%e*x1|=w+{;m;l|pi1K^Ke zin;7Op)5Q^Ka=u~1qk6ya1UrJ?J9$)zK@T)t&Yf^q9s#g(ZKl1khyO-f`&$se5F8b zD~+Yc06HEMXwE%8%$#*$Mw}4fNGe2+9_jn>bSNOg4|q{nWOGp01I!bICL0gaqk&%? zZ+&`Zra|sv0XjB#RFJPZ-(sOMrm~K!HNL-pOC(?N+onVt!o)(W@L%Jj^b5o&kV}N_ z7(x)YrPLMOfF5MwnI52KiKiaP7nkz=4z7L$)pZwB@KpS292{rRQv0@{Q$>fFW6+pX z1^EMx_dEho-q;+Xk=h!-32O*8+d6gGCvUpDH=|XWc!|!y1uO71I)j3bhJGa=naWqo zMP7QVIaLOoM~|b4Y67q7<1UeVB>5K@*%j<;GKUcjMS3rE!&y9~2v;cE(YHuoW)MF+ zjurom>C$=6jTOmy9;;_;OahTD&P{FZi=%#JDf15Crbbaq0a7Cr10hx08WgxPq{H`t%oV zDgq3kJ1v59<;I7uuCAwhsrv^8t|TS#qI67tD$`a$0(4SSqhEU44;?&s=upF7Sq!8L z2Be9ywDT)t^=csJ@g+{V7(bkKHBh`3jc(!QCUYUkjFA=-$+2aS=V*_bQudu(nWOO< z{dmEV~A6Jv~n}br&&79B%tovj%a)hvOUpgFv%OBslnC&fIs2UmoRBQ52XpeJBS1O(Fri z)UJve15s10O>`Gzh{PdQSLdad((HH^CMY`<(JZb0f`kS2J1+Cv`fJKD*hkif>dMGz zRYVq+23v*V)U|hXe1p13%{nBV)!E*@>RjJp`x-``tvo*AH#glYpOhu^BJy*dgN4Uy zjd!X5ya5=vyPl4TgCEtLDgUE~58oi$OBTRYIWJix5TRGL&&YW6{LRvd`Fh^ZW??(` zvjY&Bpn)N_Ok9bO_x0#dGW5)4!{KSMC1WS-+V0HVy($(QZSok~& z*7$s<(tJ=)`nK;8-h%cPro3R?i~*qcExw_DfsV@f`{D}1Q^T?1thDPPohwR-K&} z4Rr;{E7jL;%*wtsTZP4;9hGF)n)1Oc=dTd4y5mgjUPcFtUz&gM->IdgrTnt>ON&J^ zB)Azr9RERvhm22!wNtS4O2q-iXn&&xS4_GOdHFJ%qWRRSZUE2K;wDb^n@3-IWxRk) zJ##`LRu`kQ^l`VjD2ho-8xvV+dRA6oe!d=`HeAHOCvH-LH7MzZHSxns#<6zSIZlGfZ@FT*|InX@+W66Ez2N zcNpoIEkEQQ<3z2fYN3)Icp4lonA&`AvDqJVsOlSbKR_~C$IOR)#YW zM8KhM)gSjLD|_L~MOj}O6GC_k;U}^%iXK4biB=S9@gjQjmQ37oIfGpsDKcq_=3}K=7QN_WacSINO}{_b_`+z01nC zKoIh%_6N``Skx_EcFycVN0`6v7i1X+b#=wnR=5x?TzWj>uA~|E^}+(vkB=aHF@Y!M z{{1JtU?$P-yeKVoxhKMyaH9}|0u#7R!B5ynCh4+;KV!8MWFD>S$AON{&h^+KUB566JQk@mw|oNuq09J;iHe9^6E)bp+-aR6 z3G(vxSU2>R96|^9gVp8S!(BgA4|^ZhTcDsU-3an}JyXbt|F7<%0_^PWWlR!n^<_=G z$IXnn2>PSY_t<>@E>6o2E!$2LQ`4eFnBxS{Bx?F8IV}1%{lUepcC0u39~53%QM|oH zlLfJdPp#j&^+_*MX*SxsK6xY&oJZwv3Wn7dOMnikbKw7HPfl(b0l9MW6PXzbY()2jSx{^PEnYhwQtTS1(1O zTi>=>u62nwa&riD+Lfb^<7ThDrA%!n@FYRt$WR>p9?&1#DeJdJe#+OH(OyOn(L?~G zy}-1C6St%pYyr?-4pGvFUy}%KZtXIUQ1X$hB5P39^hP@CA525e12O@mtXY3wfTd+Z z>j&eA{k?e+gGoY(!5=29v@Cb< ztXafn`Nz($p2oyneH){#ZT9358?yF*N=_Ga=q#Lpd}KWkUrzk2rov?utdQr3a{16W=FE@eLW3-@CXF*QJkU4fG25nrH z_2{2T)R&Y!7dPRj^kCvf;Ue^a((;GfvST7@H20k0j|JA}aVQ($w4;Z05c3oa{TdpTfNSrm_d|Bj>RZP~?Z_tjWEJvA0zQo^CWQ?b?>tV2{v^S?K z^6S^hQ(qWc&i1m4HB0&YFoTb9Zlc8Ip7R4;TS5W3buBEHMpi89#q&FgpE)4ts=j!x zFHa~qq-vnnHU1hc>ELuTqVZy-176b*FI`qn@a%=5v$PWaN>GKR`HaVPi!Kw{arfl- zI24AF;QX9N?7~9rR%Rv{|BZ$F>NyS04QD zU;Q1AQ^fI~bfRZ(+@?w)f2z}_aDYDOzdzOK!E%bVIFnadze?JGhS6Hlel=@ap zR=fd2b^#~oAx^!8$Y3_~wfB7|I;_gqLcd2$Y+2Vu{GHM13(EYai{FJg<|iTA*mas- zWQ@J7X^i?{Uoln%#oe7>)uv#lyu3Vvp73yRVd2>wJ5$ftw|1?rwuH=1fL`BkYdgdJ zys%^Ky4%8I?Z%83u8Rz(AcF&Z)9;@a;uL%h=gF_j;<5Rj3&)7-4+$!?>c75>oWEe5 z3h}j~mBg4grv6eLerzTzAi#~7QfF!4BU^XH3j3E~oee+2ZIy!J7`_IA0zgr-Ut3#@ zeEtK#I1pWb1+rWWR<}kwtBYtMuUALgTtTp*cDa)?uBp%DlG@rnQ_|9N7dT-CM`NNDy0PNc%KtVf7OeJrNo z?(B5t5xxsus~&s2h8Xd`wjWxUQ&wD@=|LXQ4KO^INO68p`u73#XjNA7jK2VY|K6Fy z2@Le}ulNGBiU1UE{b2B++7atn_WC>8C0VM&7cD%zmHhP%5H5%mcq=z1r2L!V+c-3~ zZ_r!zDUI!CSy^4kwR&liIAa{0eqW4yoD1#gtYbaXt{aw+s_vYv4r!gH6+R3ooTpnPLelE@R(91ykzI0Dl-2Y^D zb~fDkS98oI9o>#aq85VHrJ*EADEO8NRrD-vf9ChuV+wQ6xhsQtIt zk%10S#pgpibNzeWrfgFtc;-li-B-V)`^)n_^Yf$0i1<}CNMzdWzGty+x+0JhkxG&E~z|t7U!b2Oz3bve@HL87buQm(Kt@IW>`k)Od+z(;-Fox(dDko>W;uWF7 zs;b6jCs~;{i9auHeuM8vj6D#e_jsci|C-~YzAy{eS*tx0G(~>;;q9Cd6RXZ`2J!|( zK>9><^e$_;9-?sqgM#IuTTE4fk=#WBY$D?(ACkk6GT*73D%9q{@Tyh6%^PQA1HvYk%afM4$K9IiF^j9rkyKy5`JE|NyEvZX$ zXMkvbH z?>H2lt{*(xR2s1Sic%B89R3LX0C?S$-emCEK(GWQ4ts3?cmi$Kx6@@`QFs-tR-iKS zT$=No7zA}gNahb%z*OOVf!b#$B1i!ZP9k9TmCod|OBWb0p-qEPv(z^{LB67)a1~Tb zn(xHsGz<|Oz@|K17Kfk-#FMar=H}oK4p?12cRL|LBAAUR2k=xQa5yrVnhF^|OVW69 z^m1#vX*l@FOVRl0E}&C1Qq_e;1a9(nUgE^@u3oAJi2xjd)@DzwY>3(tD$cS$enj=4 z!H7h-8sMWc*4r`R0?|u^?_+dy3^`i5$++mT3A9lG3Ga2H?_66;5n0d4DF*eL_>)|p z;Ic9kuFXHLp&fJkN5%+?$1;5=+AetR6c3hGL5a=eBvlHV22giR7*@8o7sE@rMm;!8 zh-GW*QY)a1fY$_xft~?4Limr)SbSzOy08>;AF`?u9&479$<~>(opdVjMO+#z!g>&e zY5%c5;`c=1<`qSUU)X~V4bUZ<_o$$W2^$^rj|j9?m+*lnYk@uhyM?mtY;bI>2^jQn zl<*TP0Ai8p|DfVjH-`iKxTni(4p78{MW<{DPy!x2(-Gny5#9KY>&M2zbz<0FdfmJ9 zMHdfXry{*kbw>vWE32es@z17neLhZKUR=8;dfKYg7q_e3_z|u>MzuIqRlhGFy^aG@ zilFE{Q6hi(>sDrF*@%s%XeOw&>)Y^10}GXO_(-&tLFyCraUseVT6P#1d z?V?q2JFg54nL%jyug*K@Ab^aA4y!bjg4zDu?z^z@Q%u@={Y$Ki0CPd zLaYN){K8xj+YHMbl!k3(7nztsD&M{(GoH$(Ew^pkW^v>a^yZEbGZH2dmKh^(!`(&W z>Uj=-83o8#I)p7juQCzSY3x@52GA6NC=^h%_Y_X^qQ{YiN60MK@-5sg2xd-> zrN75IS<3@JrxNiK37!X;h^_wsrc?(=6#)98M-Y>j2j{mOWi780y|ODSkv~Jy)XOXX zE(Ri?*kcF&IuLIgupS-9CU}oH?CtGOtNcs0t_96pvC0`uj>!N%$9t5t&++UNHbk3kbrkj0A-4G}wEEeHxC(~$*+eL5K@@WP&u zc-08Nju9eyaf|}NXumc<7|7x48=^(L3>Vkh`H~Uqz%hkU?FeEvXi*RJ*E>m?uP2kF zPTLkz@*M-2g?WQ)00fAK3iNx7*y?+5OEA9lDwci4yoQ@Nk)Hzrpx~^lCQ+A=ia;7? z2ZmBcp`Q~ovjT^pQ~)WEfQ-?BZ=Sb32SqV?^6P?XMtb`3AJi+#5XS=C!(Uv2BvOdf zlGy7=B)ErPL*)cM?02dgDy9kqPO~!qwOTH!u?NU@! z)Z?7$QvqZRt^-qz)uf#p#dn{!(R`p(M3f{zgh{j$gv*A;JY%+x;5I_V0Exr?QX~p( z!T~sBYir6_1yw{g{tPF@@$rvZkj(`HzWX*b1m(XXmIvR%Ic&B4^g(UM4kJVsAQMTI z%*YTyVAs;r8&+J_Gq%=~kWIje=!q!e4eX{<|IG6)mLw!9l;H-?0zrFcvkcoY@wW=KIT{Fil8kfo zxq2~u;65^}3EIOHgC9?g#LswnRrmBL_YC&^`N{*}4{IU`qD8n-kWZW;>%@f2sEt9` z`=s}P&Q9&53fkaXR5*^%^n3#yAS@;ZE}PgOP^Z6_Q=al?6!=u+#tHPR&3VZ-pLX|Y zdY%WXwoK4bI|D-iyS4g(U@M^cz^jB{>Wv#YOYZ}Ha=N(GLtTlNimQfsc6Ro8{`(uH z%1(h`Vae6MkVpZr$k%*u`q%~F|0&`9%0f7Y>3@JSAi`yOx zEi+>^3n7{YoCsGC>IjI4dfkutq5aZz`uHp!o-s1^44@?yT-Pe);pLU0zn~hjs=Npk}%582f{&y zAau(Pr)@(8_VdeiMEn_FC0Q{5g&!J;bAh(px@Ct>auE5hX=W1K%Dy}B6}1`nU8bfZ zQUXQA`Lbq1OzA?aM&uI2WA?=24Zb9JGtphUE`tD&In%EWS1yUoC2a~QK{6okQRfds zE9Ei&BCv$0hd{Y8l0Xf`OLJ2flPl0KfShE8^xlmdK-ITz#!xJL`xQV;#;#()L=1*j z<~Nw3)f4%bXFV-&3_LOx@)&mq$Y1b)>u-zIEpE^wh@+_c@?|S19YWRN4uzTlb#grF z#{19}z}iI4B@~QN5Nm8fJ+(6A6O(=#B~He_BLKAT-@jjITf^7JL`<0YM0$|6Ld)iY z6AbG7v}ex%e%}1M2g`-n(y^q$6=g!q3ig{-ufYmp>nmSHpt`=enHr_PodLsxjIGD6D;{5}9C9E`?FvH6Xy|L~-M3=cX33Ih;F2SJ z>qzcEo!BzH$rwF;w$EQMZ1ugp%ekXV{!61M>*3f>!P#+Ir53Lqj zoT?Ykg@H&bYE7!k&o*cfQ#vsxp}@Zxhp4A$zz~4m8?m}TU#UV<)o@ndcV`gywc5eK zK?}@G$7#{TF}ob2k&1}C5OlT$SY(JweDf)9&0fE=A()KkkFvR~M1wxa_63>>lhiA+ z*dtKBO$WZO`j}o!${8RNCG?rxgKA}E zWklnLyFXxZw5_vKL`39>t!64CnQ+Rq*O_AjaO>HDn0o0Zgga^co z0L-aurW*_^i9EoicY8bHW-B)BQ5=J6kBmJ-c)@Su@!uIeIPh0i_I)X3xW1_>Iq=lG z4}5{v*4DuM{>z*}v;n9oaw|jI;&Sm)-JBs-8^obNFS?YI_cLCGcMoFc$gj*ysB4jxjU*w()+Gq=ikG%f&F2jp27$%egu&P$H<{3(!lXAJ3O)I!faPp zL!vLFaq^t|OHILEcn+GBNPa%&AKI!!d-(D9=O<#QIpZhQbb5uyjtO*8uTUFfSLe!B1Tfwh<)dKVV@>` zUnsWVY=lixH&G%Jbs_{XG@P7n=mD|}ZQn^oLX)83s!9`RP~H5&$3)gL5ch&M3Wrb7 z?m$`yBL+$>nHyuYO>-@{$e-iaqE7q$(GARPGV~f`a5Q3Yu_+Fu8ntHVapHGqfeGBQ zd-meEQjM(;_?VkFqr09Rre)Y~nV_VtMuQ3&pHyAe_Ze&jSl46{DK39?+|J;Ms$hkl zTb(9SnM|zv(-U;yrRsWnNfa;SJO@!-!1kuaxDRD8K?=M_nSinW@HRUfU2X~bzIq*( z)LqyHjWcv5P+b?gkA<@hk5COQ>D6wg`L$l2NO?Y6u;+S(J=MDjs| zOlZ~ds3aPka3_M!CkgH7>)H!Xw7ZwmTU%M3@${S@>Rd2E@bxwHj%{hR%oGCrJ)EpS z`2@)#l4V$fcBw5@F1_^5?n$8{`7&XA0j<1S*w})g<-xZa$x04*zgxZ$GJ-vq=A=Nr z-vOJAg4}s*b0IF1&dxvWM}9JA!ZeHwdq6dgZ|E=IFj)C_(qPq!JOKiI?`B?n>;OF-A#%VzW$Y6uX`Y)U%>m5|GxN%yI zi)56OE|YQK64mEa_^?n@9(RssBjj;;X32T>ml2=A?`xR$HSm z&a|!L&fYRVY0q~Xpuec9s)9D{4v21C{J2K~;+|@3%R$IhKLSh(3SSPIFfAL0)k?(=~-Qu|&6>fWB(5uO>)YIiWbdj;3IMeka zncIJ1YNpSUH<-uvIP=l^FvwA*!O)3AY+;9!rFnrUjij{(-JYJ$p1$uk>}YB%Tr{8TsehiKr^B%J z^3G!=4si;SC`h=txk-r+OZ9@q`vrMWsV-Paxe6}=0vaF7z@w&KV5^Om9WPtIy-2fsDqa=9SHSrdZTEs zgj?l%po-r>oWS`iup^@pIk1R9G8P1vf@yiSebe{1>+{yr^7~%p`qQ-<){!Ho*`BE( zcWTGe!?_ZaJdT>Uioa3s63sO?NeF9NeaYI{XZ{@}^1ZL)702+5GwVqR2q@~6Atzk` z2=uxc-}ZuI1r`5~tm;j!g+a#FT;vCNE7Qn=Uq-h)y}x5UBDuF_C# zxdy-+RO%~P+dEd4g-4pMQ$J$7v*PCc8z28u*)*qQ~zTU>o^R=-8X;PuV4z8*U2h>e8 zY4=_XWnA;^q!HV*=osk(vb#Gd2U%E0Ung$C%c+KG51Gq`N6Mcyp=u(DIJikI{^E?i z0kelIAI2of`pvpR){JoFA+eq2P@6bABQN^XCr0#nkXjN4d7`X~m$WJ`T|!IHyzS^- z+1<_t4RJsF$`UIpE4Lat@6w6BPa8dmssu+i1I{MY8tj~$@e`4s*FsYziivN^zPeBj zgTVtlp$#9ZyxYg6xschJhmQAT8ZS--!MII1OWSeaz>ZuCc7I35J0Cu9^cLUXTeaNs z;&BqM$iz=6hRwXnbyradPYh^bqfi;_dWmWl+jaijO&a2qz)eDqha7`dl4JwulML$y zRDk%$`5S|2Fpf0>Qxdm%{lfPc?jINrT(DVi@k;c3E?(V^hyft>wT*CCtQn#TfR@So zrI+T&yQ{Y^Q*A7Jy(i$0#O@$zsSnjxhISrv_54QdIW`7kxVS0XQTPE!ya;%?J_~24 zK%p{tWLfmXT+v=wQ1A-W-(=hsWWiX0Sr}^fnK#EwqJVZ0Jj4JlQlc9t`E!u?U5fSs zYC;CmM{kdJ-h*h8zqkT#Z|1%Ex$3G}eo4JDe~?#T>@&Tjz2Y#n1eYpfio#1b_eMzG z3-_hqaz>*C7uv4}imEu5iFFo&{P_KAgvSDf>nS7r=;;%8=PI84&ZsMbfwlwV*QS3&*V8QE%lrp%C?A+$j(0d(&xIFvhoqL z$H5u5*ZXc!$j@=N@Ul2Ilb9tI+LSQ;6O&P3Khp9ZPFsEx_I)WPgY)Jr}S-}b(^PN>97YZO>0l2Qwh+b>8L?GI8szR)Ddj24y&Likei>+rbCF1 z7j8z%9;LWCC@C>~fv+q;5uv*gFzIe!V~b?#?>CB2K6j~*y)xe$JO<1UNOC-z2~l1G z4)_m+Crr2U@TlorK=^DOjvUvv^flNUxT;(C@CS{?OU4{YrOw@D>uemtbhgw#@l}w+ zfaWQc^Of)Rhds)3=k^I*`@v>AVpQO={08wmB!_?NvOqW^1tAF!1r5{-L|w1j+#w<; zxY^%d1ut%9-}d(hqwkR17$fAy`1<;~f1(&9YJ96^LNWgy^eS*u$d^_;jz&; zMVZvSMKUsngr??I`lA_{4nEU@1~>V%bF=D~PPB7f8l+zc4L#4bxlPkNJ|h=_qDOWL zmUuj@PTfCM&u4e)CL9<)Jw9A%TV8Ajx~Win3MCm6L~1}{BB2ijG>go|paK`V#eaC4 z37UD}qCY3C+5$YIF)?kyz@x&`to!z{oWEF6rWc4J22#sg04-cTbFN=sg>wsl%9Gte z3$`e^q1t>2%QP-A3`#vT*&PC#Zut3?`(FZ!7LpztU3P13=8!ZC7Znv9y0_OtR&TGZ zDkGEN{OpNhS~_yi0z{BCII~b1Njrcu7rsfzBWq9!ajC!xgRY1JAVM-qhNTvtMU&$@ z;^+FQ<`(zo97*Vq1WFY!sECe<_}2sinKNY=1qeumyCq;WpSH}wBxaK>iE*AAr-h7!6F#REF8)tp%-P;o%ug3+scWu^C1 zBj~C&)T8H7lA4-fTH2?x1GK#itj;sv-W2+t%hEC9I6F?SE25Q~``Pw|)W*k$uR`|y zhW=+yvD3i2m@iwTuYgY@5#w+c-67!-!w&%*!S&4*gg^q+a6usybvGd5k&NEy~@CWZvvvz$fGf0MBC7ymYTm7)5TvahBEAX<;hI)B}U zJ`oia9>^(ioRFX%)alMxMk7Ejc~&AW!<(+I8+Ue5+ohKmzU8{~&Q4r}Vi+BTEDQ1{}*;;rLYAY&J)924A z878ULAp>r=*FQ?$yQU@Ct@veWsW+ZBDq-*n>1gKfh3a8j-%3bGnD~SLimTfZKk+{( z`yUh9Pcp;Z1T*Bgo|VP>({66pb8~Zjd#92fAKiy6nxV>%LOY7r%+1iS_G7isf(h)l7vvKu`Vr+-(SK}F?e$%*}wBTd%~ z^pAf|Qn@?XqkTGKL;GE>*W(>fBb&19H~| zkl}ED|Be8J@79q6J%g$f%ejgSu{hrg#|4&)Hh2x(eSt2Ds7r`f9&cp7xp_QJC=*1s z>hF66=#5y|ph+g7I~^z;?W`W4qQ;e0JwazrR15<97B? z=Kgfq&QFx|Q4E-W;-Gkmm{iywy1k*cssmh#$P5u+&whhNYK7*NaM}Qegv~7isbR<_ zc^6%t4R8*m8xI2mLju)5c;*})9tgg#qBlXm?$c?9YD`Pt#?6y^p+!3~C_~S_lG60x zOLfSZIjQQ}5VRD7}X2L@w|BsO9>#ezY3ZT>)5=e;gk zA0l!>^~pq{lFzXO2hRT%fiJ`vP#-~UTX!@ahWc>g#mlu2@u2b##dFO^8WDjy_jsli zk57OcB3&Y|ZcKCl=!fF7%`C?4)Nkw3?rXZC zR=%3Vg6XA7-t<4ZMtw_tYcjJ;6-4PX4X*0M$k{Hzu~O;_!5M`lVnYiD@7`l;5q`&G z2l`3CDY`gfj{|;V>(urM-6N#dS8+;!3<(^~!|Op@6%n3ekj97h_J{b=fMQ*K{N1R5 zX-i%((9}a7L53OOYm$VVAm?=>Xpeql1}+dl0-1tz{ajv$D;{0OLy)pSR}fA@D55KH zRwOIMs*a4H;=XnFPHC4zVk+$sd4V z63==X_9&AlN2Mojkiapl3jnGAayGBV?_;b&E%F&lj>u1yqt-Do4FH_qsupk`hpp?y zbrMIOJmOJ^{|5Z0`DBo!XGv^%D-Qi zV>hA$fkz8j&g>Y)4iW#2zoJqQu7RSAMsh`xs{eaD@NGzYjXSbmCt5XfEv14D($F*j z`@jXp%Wm2hyK}>RNnEK&d?o#Na&mI<#SXyPK>v^l4f!t|D8_8OEVU7_&fijkZ>?v^?-l;xO<^*y(5eIb2fe^}c}m+fb( z0?`q@c)^HGwYh#i3;%x5!JZs2+m+i+dnbzT&6}`z!j+t$I)uN z`D}$4>Q)7Xl7BYivtZ{*SRI&BvLTVti=JNqYD@|^2)OcZ0Q8VQX*T$|K%I%hh;1uD z`*~5KpyS@W+x;K{2EHy>fgiAM&on?Zar+YsS$vVH-)%b#%|Ych-{duDDZ98HD(3MY z4)?@Z_g>6A0bzt;JJTNLwMQ*ZeEp|!V=Ew(Fw{LUb5Fli|^ZjD?SJvw%btx!P^bT8k@2jUh3#C4Gg$$(~&Cl~=*tK4V6 z2z4}+rx?Y>g^@P_^TV9caXTHGr@k%8o#gPl&iUrfw&25$HCN|nKL2hv+|gYS_7%eg z*D$sgGP+zUc4@kDnI_dnUU3eyqUd_f|C1d2>7_BOd)(Zb8j$=koB%=+R0*@T8{ z(*HH&I>@+d-0}~jrV&-aO8kDX0$ zD7aJpaJWrbjbnyM`erW1Z{Ni=%ltHg*yew)@f?X{Q(NK>e)@EzZ*JNm$pv?l1I%{B zBTc%&>fT;!Fn_FmD&-{OVI$HAC;gZ3s^A?Q+VU?fG&BNWbp(Fb^&g$``y=Zk_YF0< z?Si%k4-E=Sae{lG8)g3XpyC~zh2L>kS%^+xItp@8Ewk z^b|QZ-`MgfUQ$fuv`x+1+h>L&GMVRoSx-#`mHXQ_Z8kw@H(cFlx1|w0K^Sf9W->^H z2qAF4j$#*g%OJKyZ_nU>a~?Ht>vUwWOWqgsi5JBQW=?Zy^dNVrC{fs=Bp1?)v3 zOeh&Jm?ylyLLGunt8Hf1!BC#R$^-Hnj=9mYC|Cj%0aFDI27pv0BpF&4RMqc(em*mm zf~>o^_q&@KQS*!2mhXUlSsEoY;#692EYA2qo2}9hMt?{pRjdT zD}J+ES{ya%d3R0CFzI;BbNkn=oladNO-8R@IPaKxYMs!sufSQ#fp&e#DKa|>5|evK zfCEOEhNl5NE^#s=9#9w6S&`EvkaF-2B+E^|6!!n)wR`2x3OT6w&MuMu^Rot~5NH9{ zhg#4@s zpW?wuNk)jG_}lX-{N?=bYw@Qo>UPP?A4Z&;4rT`#5%>~B$r^Rk|0_8j8;_Rx6@i`) zm>h)BYpU+w>?8uJ1iVI@MM$o0YszJf3==0susYThhf0cO(McwI2M2yIXCZ6}Eg!(U z*TB$TgSdSOgkmBgcoQ7k7c<>nG~W~I$k1JLTe$=#9p`I%S_>CG^)O4j+{~ntGUQ!v z=OX58ki4roNoDb4KI&3ya+Z(-Vb7jB96?gMcXJ>_^TK4;SOpaC`v@f1s<>*!D}^y( z(r5}~G|bJnVI4l0@QT3^Ukx+W9_8(|Q{NJJuOcev_^Udwp*552qT$;Y782t2>gfr< z9u>eE{PVNWU;RYUnwQ=KU(&Q3RwKDJFlzDXrtPAe*6~QR8Qy2Bey0<@G_UphW3Ge8 zQ!57~xC&11)H`rMOKT1A_irO3qIEZUw$Hg;d&w90GhxETB0BvX)Y_aw`)LTq9D&sl z4i#6$G*^-3&Xu`AR$`^b`TAag*E~P{0n~u)DnkQTpifIby4O4NQMUSb@r~6Qw%dG% zn{(%*lNC*89;&*<{nb+&nX=CdsHUA5EX4ub=~y4Zdt=M>`OX|>TQ)?Bc=mQ%E}c5D z?NY#s1tWraioU`lyN`=|?D}Md_SU`OQZgnHaJT!ny8W^Bgh1I7tSqaN3;b&XXedrk zH208fX%?1EAoKhNr2)`yq^D=Poe=S<#BhdY5d(v+p-CcgQbam&6c=mNLJ}oSFH^UW^1MVWMUi|Rm7Zk*6jW~GLMkMf8n2HIkT`RX=d1awTO(n2=s`rKQ;(5H+ zfrE>G9X5vdDV^IisC;hIhf}8 z8t&EhB^=KoaH3XO&-Ye$;sR4>nUspVkI2$Ichok?G#ovgYZ-j+F0wUA zpc`n&d+!my4+i$lI~`f)FOB-Qvz5QQ4Gay{;LR4vru8`iYyi?I2O#)BQ$im!Kw~=^ zP9U5V**7*m{m+M>xo=rW4OKdT0=K=DfVXB^!vr&Z}+p8$H*J8(gJkFFd#_!hwsk;qn; zLP6N#^#y$cqSQ%D8Hpf92s5G6WQbEPR`QvPYopanhmkLN*fQNx|{)UB~LH#+0K==*w$yr8vV6x04tyTOrADDKJg+g;^jXGf8B ze(`&8($2v_9nw10CAt+>$Yn21_U}gQ2)zDnS*(TbYj~8boB-rloT3HcRZG%JEOYCC z+@QI0Z$})v*);%Mq^E95pu4N&BZ1==Ak>EvNx74>-XrQT#K2REExQ?^H^FSkyYNO! z19rL*ni)BRcN-z? zdCp(!(lh`I#@)O3j6J99o*=vor#s{Xk_}IWP$z?u(QJr|rxLgB1{9ZYX^we^PBeN$ zVL%0Byu{>23ZMxjji*@3@3Tu0)$Rc@^2j8E33OF0vda;U-}=b-;Xl7$U@K&aKK0|1 z?B2irm#G`0XqAuAJ*%c|nz%0gzGsVN$wwq-C1x!Cm40olOfhM1+XG&>{;BoIk503j zTQ7g-N;$Zzx#*-;1H0JNpK1Grv3AO((eZ{c?I@{io8=qBpdY1SYbPZmNfAK5U145U z?hHL5A_L#wxjb1;qoPun>q!Pb{BM3DQq_U(LlaO=zLglt%-Ykv30p2--b#Op>H>Zp zEyL9^6~C30MgxOkpOX@z548sa255mqoPKf|MI&$|7Uh?7bh5JUz>Tna!uxAMT9b`528>6Ce9a({(K6NOwU7J*)SH-&P_D1I4?%3G zu5GZR_@xd`9UG2V`_hv})G#!V)~@02q|4Es;;GqfTU8?Za*y4iKSWg?$TM>_jG;+4 ze#9=@q!EII%u;0vOQ4hx8PMQJJ}!=dKI&YARa zhpGaXm=YbcD0ORr%Q>QjCa4+`qwyu~W0E)En6fStI~yAtizBfIFZ_*EypX%+$?5mE zX88|%=D6G{^&N&Vx4q|o2eWrQduS$pX8xCQ@uB$;K4<;GInRxqTXpkix@@mqzh>`t zMydiMT{gH<7t-#QJXV+D9w*R>S=}1I}a``HjM-0FTWHuD6qks>OxPDzP(*v?*2ysHuNRnjJs;j5& zE1$cGL&HgN6%ai9HCBm|tvE4x;&**$e0PlnUC(2){H)Wq2B#{SIqxzZrTnVAJA1Z{ zmAkqrjJ3~l6gh>jWnI2?r2t>Q4|u$cx)Qe!(#(|W>Jqr~kTaZ)P)%UFXkipkgujGE zhM=1Z$4=#|dz;=y$g+sQYb2pCBW1r&vcgEVWKD&p_WrT91m|xRdb(+m&m`}w)KVIH1W;qtDujxgpuO3RytUmT}zk-a+!pVyi z;W6)M+yEo~Z9ymw@*uH-w&eyOR>+Y)Va(aXhgZSIf}KRRgb^?gC~#k3&@}47`nPYx z!Iv4AoU$f5(i{q^ZT^O+A1E28{`_;X`kc>|-MQ9(#R_qr$P?`540=(c`S+rq*WZsj zBRF@weO`zgY65G_YZT81TrK|Mzyqk4nDlGZjgEj=Q-1;5m8Q7N( zXm&jp*IYC03tUt~up?mNc@kt+(`^cLd^)0cl9-+B&3B^fIKvJ1Kg8*)L965k+^uvB?HeNgJaLZ`aWV)@EC)nt{s3?1(?gQNT{f@GJ*K;w`3y1#o@OpNdbk47TgQ#jU%_mr*U}!A*a#fQ9y7Nf@Qa6d znOX|-RUl+e<&ixLl0@{-eMVPHW?VJdJhKmO-KsnTrYXej8TtJqq0xN0#70>L^&sKw5X-;Z?xw`<~9K?x*T-Nhc)F)j=} z>mZ6r#8pOt@}eYsxlID4w{5$I-qjc)QwSO>0l8v2;iKJ#iShCA&POpB`%c-}Ya?!5 zX_sah8IB#Z(%Ap*;gN@w-K+A!>V9NiJoNSnP^qTm5se$^NP7~JVa~L)+>Dz8 z`Xdt0?;*J#N+I~JHsgrbX1r{YJp{o_;bPC?)X&edz@+P+Kec|@nkOU%{{2hsb>y0J zNTk0TJvS@9_X72iBf*NwLhkP3kIbsE44f#oWz;D=DdJ{~5t};CZaHT0f%Z|M&vu{c zH&m_=NDWtfRF8u(n`~?nJ_eZ7&dn{+DVgmGIHYzspdsZ=JYfr_1s;TTDHekYjt}}P z3JMCy1VIQHT;nY-BK8w?IT=F%A~2YZI9k@=0+aRFEWclaj0sXZ)A8wU(sfS9&6^IK zh>h5M`?h$J8beXS70Um$_vUXouj~JRMTyKZlp!oBLxv(#M209NAybo3GNgf`C_<>D z3<(V)Nhl>l#!#e`RwW|M8PlM-)aP-n{r)Gu$MM}i?PKq?xbN5fy3Xr7pXYPfQ)4al z`1;Dk_h;|BHccJWTPNWC&7v}W*)9h|hsuLNj8i>&V)mqWj?X0Q8!FqYO<%SPia8+; zP04?O8HK$)mTQYY@sp(jsR(;&n0%C+tm^2=0g~JA#X<_FgcAm(^U#+04>e{CBlMx3 zB5xed9V`7UId2Z*RumQ`XAsrlJ9cqpZ$glM{@G2h2Qk@$Sd$V1P7AA0tmnZ zl{PAh^C~|+7@m@Mt+P?QIK#6cD`LHI)TmF_`}RFMzWx6AQscrUQ#`oc1W%v6gO!a> z4k;Vhb_ri~K4uBdsUohgtkwHax|^9(9t=`p&4%kW+e9Q)B|!dy*S7uf-b}SwL-vJg z^rRteudkS_e}Bzt>$b=HmmJ8A?$t$FUNy97ob@}2v19KK>U}W$*0}If{*&}O4w=;z zLP7^57$-h_2wjKoc5bxplna%!IO4N7;uZu*0iNx>E6IyF23b`EcDHNl6%-Y*tnRqM zvZR-ktLr1Q+`;v)=F0kQH@{8cfrxhuIy}8J)B=z`%cV*)l6OCwFf&NyS?|KHy1uL1 zzx8T<;IQ>~@tBnd>no=9?KLMn*(b&1TF-e4Y&txao8h#4^2h~QT~1!Su}8B4ifGkY2R0UWEdtgLg%0Elao#%QXZA4+yVr9jdI;q` zxrm~3>!$ml`$u2UvvdJlBmePoo^b`lMN z*wnDArXY%4+MI#%BUMzyh{B!=I8<3n>msv*Ko<9?sOXrOtQn$kY{t&c6W(Xdb^5wA z=FXw}Q@4jURMl@+baC05uwP^3?EwnCPO9{PkK%r+&(!HQZGFdHGifwwo*=aI0I`sE zr+)eil1uUsX?H5A08^of znl~T0Z7=@BFGGHRK5%KSMf$}8lA^NO@?G}s_5Y8vac}pu6n-lVBJnar4uaxdNrc!# z#3V$yu(-9UFcNa7BwHlXh`l=Lc;0hTB&B&n7>o*; zyoMR9?xKt*AA12KkY!#s-W0ZUUMPNgxzm`jLpZ;syb-HHyIm&F^WqQ6bo(K83vlo> zcFja~v-!gFB8=^0d6Sw|84WK+%O&M53{<>0mu_9n+K#^yHJ!!j)ERoBFhfYZ!-#FTJmRAYy##rh?8qsJ3jyD{_Pw6Xe&OS4apx;SK_ zRC)U0FOQGe?R@rV_JOfwVqJovO4F3_2SzLkImv)70>~^<{H4j=bY~3X6$+_{UD|#n zIyxisvWf_cMg5r`J81QS!jre3Kcfwa*KXx)y24%{Fu9NNh!H!fEQ+nQedgaXO=5K* z+x1EkyOI#?5bc8=AMM&_)t%Sez+L+JX~>?k1Dw(t5)1xDkAF2~)PEFq)PlJuvZKvB zfv}+i9das{?)Z(EhGD}lQM?YQZyKbrue-X`Y{GZ|rj8yvmdNR4RCC?8kBXDxud;($ zh|{M8iLMmfWVUA_4HzWPg7*F*h~+qw>2cPXt_;X^nPy(LuvuIh2~7f1ok~moA}LAv z)6kX>%6L`vo@d-H_@KX-(IJ^bi`j-gf&p|Mo9HA1>$P1QdjiQFKRaq}&KwWfDe5{F z8--!EA!d?vc@Wo)qD{9m4f}mhmUssHu*Uvk^oV_S)NX0R$QAg3cC{?AnzXgvpY_MS zcjxc1NG2vrEM?(Qg)RA0;{>!&Ob3R6yCLd3A(7$oif<}W7k>fRw2jiw@8A8-Ur)(f zNymbUcMfBSVbpm4)1~^C7u2brsRLx7TdALGCe>Md&5sFn1_@5SWkoN%7$0XEIsJTy zY}OE2IQ9kp;c|Q~WZnzV#QdL>i`0r(Qh|k;)OR=lpD)q=K)&tdj>O%LA9A+Ci;2)l zk91Xd_A&0^sFkCS@}O^T%^g?KB&c?jfrwSS- zask1W(Fk{Lj^MIoRfvFa>D?&@As(>a+obR(@DlE>!oZ&BjN|?4idgwNR-AuOw=8`5 zzIR`Z{jX#o4K_Q;6$a6Jz?7vVtC(+zETlXrxemrUi_ol8F-iTRS+%4;sCm9pF@7Y4x4%;WWG%L zvtF3ew5vZI%78I-Mu277fWg;|J+G-ILTSWgy(dnudHwD;Otde3z4)fPLdkv_;&`4F1(XsEMYli!R_F@cdwN2Z&SED)L@8Q#n^YCAaoXObw8_a zZe=J54(1v$|Gr$gy!^p;L>S=N!pOW~7h$ikwsP-Ewf%k7$j^l#W-rhTgr2w!+g)`) z=|fT37wbJuUS}u!d0aG;>4}M&=^tk`=6hb>V68)eXo^tu6IA{>Hvi3=Wh{!)|qHsee5c)HllV5WoG< zylQy4pR-F#4Cd}I-_C(f@a4?;%PQk{)PfNUTdUA5l4%Rm29Y%k@;C~{D1Ui|RzTuy^{_);3Y>@@2RIqpvhH+uyxCZ}A(r6L&1UeF+NZsV?t5y7bMn z+^~FzK}ISS!kZaCmWdhf4Ued(t#{R2+9_*>q(a4)IBK*Jp!-mlcO>p{FNC-_1TKCd zB{C_W?w?7#sd&zqkMu?WeBr`C4jW>4{|_HdTeZ~Z=Q)&i?767fe$-3P&w6?xg6;(` zJMecfbP+i1J?Zw~gQpEj%uG;+x2YT9f=O|AB)i z9-dUKB-EO0+Jp-N+bdC5$q|{0aXncyN}RB~KLpgayM=d}bV`q(RArr{_CdENcY(KH zs|Qh~a6w$ADi7ARCD-k3I@u;b)iU&rLy;06I@D1hF~a6YWAoyxmY^nao8NBWGjLGs zu>hbG2jmV1LlALUY)U6k*hJpP%u33X?65n1+!#Ek5+&DFR{J1$xrR`zO`SfhBO2Zk zUKG|6p%D?DIC7)IN<00bsS;l#k*`MUZ@~ru8NCu)TIka-$sDd04UvFneHnNjfwn!V z_w1-KDY=TmlP6e1yvo0~OlDBoFMc0c6B3f^HVYsib(n7X{!7}MKm@5KywgR#J?JIMo&yR;Av!~3W298wz}QA#dUOp9DwVq54=Hs zYh9s#@@QAm-k(OlF6_*>{NBp9N(xkoGjF+RleBemKuTd{79CUOaB!vpSGb%`n(${`0 zX&9;RO#5fd%Il|Tm98;7(uw8BxSN`q=9S6trh}WqJimKac?lv>5@#@SdIo`2P)m7g zWgvl6Ffmn;0`9{`v0jnKp&|k*UuM9GSzv73Usw0l;zZCBpiO(M+4$_jR-Z;DLNojn zFEa}NtAvoUTr9g;c6Pg*f9)o*Kz{>5TIyYv)s_edLu*}wf{*^;7%_#IMI@3w4;%0Q zbdrKp$ZjVY;XC~gtK~~;%1WX`Yk0c_h|U&zwRXjI{-v0ficZ z)5%CJ9t*-TIg!5*zxMC@^}GJ{WCt}uU0q$j*o;4zi~rJ^h&}E55eZqtMd9S-)wyzX zy`aJ|6X0`vj+bKVx~||<(I$_Dg#>>A6O}jw=c;_?J>jeuvM9%1v_DV_hb!*15qv<{ zyA>rg!tZ!74E}_32}qUImcBoC`D2mk>Dw zQxkW4F~%_Z2~=(EO+kOBto4mwD`YWLn|)Wcf?*9qp%6CljO!!dgQY?gin}wl-{=(! zXS96!&+@%g$IZVsT!3J|mmpi-?32Ngus1mqTu(H0+_D|Qm#18s>q(4AA;^OWp}D@Y z=*P;p%`xTW{a`K@fBUu&bOJlh>~T$}i+EK4A^kAsu{ORObNe=Vnf=$i5jNm}k`har z#d~5w@-A`}b8b@82C!5Gm`GCk7)>GpAQb1yzt2glWeu%T`bmYjy zZ{O%o8^qd-cdv+2kvFc5d0&0U=cyI({|q0F8uv2wF7UnoN>%xf={4^&GgD%9j@CZS z%FTuHSaI`}z_D;x!))R;2OZ_D5J2L_>kpxbNtsYzmFfn!h2eUIMUn`0wOC)D(VH*m zOF-tr29RDauh2j?5oPMJqesnYi}T*MQ^%0lhi|i$;R0U*WdQu&Uo-dGh(%^YnwI>P z9>cgIT0&R^wlmZ5DquKdWATiqe9X)1GW9?#7N8hkUzs&FXWAb18>@>F^Wq2EIXbd; zo^wIInMp|U-MG%iv{hr}UU#V6mjBSc?D(LDx?UeYeza?>Dsmb^nh6RTX>cL-5HYv} z@OI7Pvozva!bdIoGIPSDjxH_pgaX(mCya%d`{uM)-!`{TE>GfEXHd>@) zUJrodjd{VDD{%}69DD3_c>CK62YTZGd^ zdQc!uW(hzETZ26~S}|pkb~HTIcmEOZL+?1kfp@uDEq!&NMCeJCMs8Kg zcGQtrH1YTF7T%_kkChY_MRxf4p{h?kKK$QyAq0V4_vH4jnVOpYP^n@OeVMw|*K}uZA8F%goj~<2yllGASlJ+pc}_ILcwLKN9q?c_;vw433q1$5s4Qyl zrY?~t3GCWcL0#4$;}3EwKkE3VhdXSc@389J! zGkzKb2wr1El3{AVrTYmB%r~F)!cSXV$}BvNvL-k)dSw{LKLT^Yp#^C z*9?kjv+*6K&;mv^V~5j%4Wzfn`=u{|cp%EKmIY@$@^{ z(;(W^Hg>0ld0@ah7VwEw42B&^S}Y zP}A3KXUC>of+XQRXqtao+G5A)gSjqnowM6$WhIto;=15+Tn+nN(MNcGl>GQ{oC$Dx zm!_DZwYJ6~7Xkx?f8gb3I*$skbWL-pN>K@k*k;r?%)v==D=F&%BFsA9II7x)f(*o}Je1Fc;rQKDQ z4*F+&w1kF6pXrWOf-=JuRrvk;ma10zQe_MYIG9lmC%eeWJ#cMO$GMsM*}-SQ3)W<8 zBRon={rx<__^)AmT+{oug^0BrIE^~ukgo*=^SI2Z!4r5jg0{+#(%w!3MM*_^+l-dd*D zpWT^In>;Z3lz)u|DZsbw!-;;iDkQ}cmo#1$nTDCWe#kRRbAO*+E8kK1aw4ak1@~Eg z@PDzRgx#cLXxIyQ7#p^e^g4Dlth6Gy4Nd2V=n(3eq{` ztgO)5SIiq&F(t|q)u{^*| zX^4@X-9%1cIKS2BC;JA@H^e)t7`s{=C}a11EPq=DeSiGA*?$k%K507QRIo4L)4ihR zUDjU)k$v=rbTz!x66PobwYIvBGzj)4Y?>Ops=zBOjL~8E{hdWpxS`T+8LQ=YP`<2> zLzOUSVy0ao?zmr`Wbe1nqvDyhLHg)=R(WKVY)L7>y#3#h1&h_0t?&;t9 zb!_QHYVDpqd&GszdZ^!6_swsgWyNhAIFU$zh*rHPpKsh$t8U2H^kao}_Ck-qHI{;S zf~H14r~CpqCmfJ4@Ee)!=w3wlcSKCgNeZ@D7R4_yl&9%(SSV#W>8kfv(Y7%?j^+)`^H_hERVj%NAU=XsNe# zE<3Ys+n25$owmlrSkGUPT^-RN)n!ct|Ei(f`Qaf%gjbb5;h1ZhXFrbr8E9fy%94 zznHZxh3YxYIbb7$ULORDoMVwJ&uP7 zB_)k`=$GAJ(+A%&o%xT0TbmRgWUN}e^PavZPhC#@@c`GuLKt>qwcjn1 z$K3C#OjfS2-(71nU$IG)m~V*hW!&EH_j$^AcF-vvrP{LzrEqAlq-sx>_9>$n3e|@D zK`CeF$!nr=NQHU^XgO;1=wh}9-yEmYi;^dNQ}6H|J_n*O<6VifF$-X-e=? zT>?I$R~ms*nw`%E3yV3_gxl0k(qb7!g#!tV0M#BqMp-lk4;C|~T+ zxs4Z)@T=b3v2Z43yB*dK(Cha{R7Z-BqIcTtlq>Dv+~Q$CX<*z;<{AftQbz|Mp6aTb zioo-wX@N!~l=C}=>iX{Z^<}VWdF??Esu)i4xAvlgeKES=x7QB%e!N5Fk5uHpbVczmjX!f#z9s1Khr}1&afVbqk&}c@LH| zNwCIM+1J=c;Ex-@a))eK@+Z=ym>!^LEsilAHIw6my3OXG&TI;RT8BK0IPX zUMHp$xSnFVYb&x71TGq4P*8@BjnCZMN=r*OSXsSD^zam(%DlPwwg34M72Wpxy<>6o zr`Sz$aM%_^c$^zBU}nO`)F--5NB6AY=4DxsR8XMAngH+e3=BiQWk>cw^eQ|*0pPu0 zI)>){Wb6@w7GQ*Si@#5}OvE?eyt&-XNw%6L*;2^~MDdB!6sF1nt=&75x2P%qOlfhn zq5R>RvYS=aDY(p4RZRdd$qcV=+MmsYvBMTjxKtMX{+%vX zaCpZu)VkfbwdF$MClX(qYAX6^T3*jE;TJw_?+QG+00)KwFnZa7%iyk7QxRy_VbC zKb=rFkm52H!50MuW+tk!Y!~ev93nvu6ciL3E58t?EK9e;;57ySVkkueY@w#DrBb`z z8QPu#`*}0hYU!e)o(i4i`pu@K!&x)9Q(4@e#LGOF@df7QLlN(QJPGj*W&=l|@^B$MYHR-4T9Y~+D*z7h^X1CHZv?s-wRP^1?rP;` z?FV<{FXjM=D z!Vo2e7~RyRVluC6_Fu~}E3VC^)06GerP=6D13WtrhQ#Crmlkx3xx4ChMutn+zJ5or zE6J!X(eACN_y){@Fe#V7qDtCz-UN|GerK)P@vthS%Jx--a~60HI6yOjLzgRBV8P_( z79rXo(|VGyon=q#$Hs0}y)y_Xe2j4;M;89-+C!tDWcT%+?E~35?8k0Hyt<3ik_x)H z2_Yzxkxe7hgn$MdEAr0pTrH5YFO9GSSrd>bDR=d*knsM zIQX1)?_I3?4~N?r$9DBG)!Ood2QPt*GJQ|`&*-O{X^3$+_CXgPmT8TQr<|t3@#lLV zkhtZbg#6T!191JMm8Dph7!1xhJt}LO>xV<5R}LMS?9IP6^Y)YDv2>@IWbdGKN?pGu zh+4?>8i-jJ`x4kiRUz%Ip8pqp3XX6nOjQ^v7|d$w=(AuhppD2|kABiTT0rM6F3jhR z5lcIM+4FUi&mY0xg5T3=nbV%Bgl#QT57Xc^0FVG_ z`eNz!717IEKqR3;{+gcj4(eV+?wsn?X!FIX#bx(YNQeM=@qIN1f!(-iQ>26Ry=@;4 z!W*LNl<(VDM3n;;?%cIY8NWrz4l*cja*RmQ+e zpi2^&ouHj5R3Px|Yl)-t*+1El-UJ~}?zDIJRs78K2B8}d6});S7R1+7j=GJ&wFqyn zs<%@Lklo!5eWg7j8GsGXwM+-aql`!B@3?e^X z8*=cufK;YWR|IBckte)}MKj8M?e@cIicR)W8gOH^scON#&!6kS9&N;Q7UP&%yx{4^#K%IG{p`mD`Wy zF$*qpVRfe;SfQMP zNr)rBwC{N26ppS7ti@78UYhA0zcVyb-4Ja90{ip%AZcfO%~?|2Nq+t?U@ljR8iS#U0)zz2qxKO{Y@>2WU#e}CFxgZ(ZHvGt5ly-jdnH~9$UZcV&$Fs7b`86D%3Azr}FaU`r^XG4e7yZBQ-QSu~KDEte4+F zi%cDfWi!vNUh4GN-b}0f!pno?X!~{P;i!;5EGvHf8Zg;Lp3;(W)v|az;Q8cPvre90 zJMV(#$$$vsf(+dX+?}}dtLci>H@uXMzv0O{7!$KhZRpKR>uk3w-`_I_LBr#_!drBa zdEDD=K93gm|L6OLi+4@{qwxaC47mQ_TtI*)oQ;Htovei1+grvF(YIW;6U9Lv3O=yW zkCK^MTKGv=Z~`)JIp`d<(i2~?sO{E_-OtM3D&PC~=~F<08Q20H#D)}qW$(c=-ka-6 zLaG+m=kMWRIX0>+m3dlA3lu_e^o^7Y`!yU-QC%)kI3#m{aCS9EDWHl z(qW%n8|2blyCr^}tUCpb-xOBiwR_kp7gtuA*_1BRa)UNpTGlsiOP@oY9uFR@iJa+i zX=7~$(;w)7$KP{G`}=FXfxgK_=^q@tV`mFDXCsIUgLJ*ocdwqsP5u&%y|3|}3CdKH zeYZHP7xbgqsZ)IO;;edzg(Wwm%(|U3wSKm%AsD8~(Kji_VxcpSY4~^po7Tq*A^*%@ zaAfA(Bkn;a+bKzU`6_r6P&T0Ao0j)mul={S4}8*uW(Ryg#0r7_T&7{`|Dpt-FIA*C z&1>CG$68J^HF0y-UwNC->Ge0sJYz2D)4TVEox^373#1|RO_+M1^>N?Nc$E*h~%iH(hY{rdIL2|N9o!-GzpdV%%*_kWYT=-83s9rE(>O0=Z)O-V}| zJ6SOsM_@QbD{XQjKvj6d7+ZA>?im%27l`nvQGsNn_w3`4Quvp(VK`ay6qa+0DH8y{ z)k7E*-kGOg<2|BsVVNF~P{|d&=HYHNAb*1=8pXEkB`sU(gBY^?{rt8}NC;qo0Yt~B zJy>ALgQmu0o2K!S)42znpI-R;w`sXmCBkmMFESULW)d&J`hOozr;rZ;ml zOKb{y3`Q>f#+xqQu%Wn5rEkyTkEbLW{k^=kUba;Bk8{&@{N2^hPggHrH#+xn?$6YV zQ&z`qUgD8b;A?MZm&pA%%4p=ju$*uc3~8+PYR_j26La_d&f-JQV>fj{kAHfatuQL1 z#R^Es|MY2{iDdISNEW44MI9QOe5|ni@2XgP;zrkfl6*v8{1y7huXtHqVs2^qhGmw1 zzJcLs_3=&T0|RLia?}lEB#AhzuB%JRu+^f}YM#>=eaLy*A_>=&2ML~@dHf;}U0CQZRVe8z_e)5?f%`h$!JFMQL9wKAB;^SpI@9dva3>e1%>%|BV4c8`#8AmZ^4Gv z#@i$ecyc!m)G{@c$De_3wFyrenGioVvCm&L#BNh)ZI040H9ecwt#5J2>|W#J!R5x# zX{_KyxXRVEm{H&G6Pw8>i8mi^tHQpsjxT>Be(HLFrO_itDu&PLAdfaB>BZ&9K)8>7QmjkC*+!svFDsM{l*S8_sW~c_&Y{?7@w5OWY)$<-D@u!P_IB>FD z(#v_5VUw@kK3-LI+2_wcXxtRTO=tEYiDX3+LG{`7{HCgk-QF0RU3%0q?s#Beq<>J* z-^DE>+b+@!jJADLov=LOL>%UL=2iE!04nB3O*Sk~o3rJ^yA^i3Zdz4+0Ja|GUeo+j z6+FKTlEHEj$rOtEb5!LMuh=a{DOT2S6*6v^=^TxiD^;ldf&Q=W?36b;AMf>$UPcd+ zQx7+I43mJK#jrO=x+zzX@NE2P*YqI9@XHc(^>O~^&NXT!3~TE|Z`viaqx2>DzL!=+ zZNADauxdLV{L3Xaz3P6t)m0ut(M`0$HZD7M++q){BCYqlfQlcYJLP5jx5o`%j1)B| ztvv2!IIE>=H~_Op{Wz0HM6b%vpVQRv)bK~Pou+kwWUvmA!O^*x>mzWyj}Y zmYjH4MM@t|N__$M7sGX!ZTnZm5;J=Lmw{`X0747ZqSKr?`jCWMvIJBn&?&!{+U)|R7Vv)YSWv#UttFpR>@kK95+Wc>O zD$?v2Sv^-pt^C9trZC-67TfKj^7ltcX*j!f)YwCfR(=Ewk>q5tapNE}Vq1DXf4xvb zUWflSLA^J59Q-$L?~b$iKXhdO`A2fu|NoEtfB(7sX_stt+oYwo;b52q|5-GD>AYJy H<_G^DFGyWo literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/visualize_cliques/visualize_cliques.rst b/doc/source/tutorials/visualize_cliques/visualize_cliques.rst new file mode 100644 index 000000000..b2ac037fd --- /dev/null +++ b/doc/source/tutorials/visualize_cliques/visualize_cliques.rst @@ -0,0 +1,91 @@ +.. include:: ../../include/global.rst + +.. _tutorials-cliques: + +============ +Cliques +============ + +.. _cliques: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#cliques +.. |cliques| replace:: :meth:`cliques` + +This example shows how to compute and visualize cliques of a graph using |cliques|_. + + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + + g = ig.Graph.Famous('Zachary') + + # Compute cliques + cliques = g.cliques(4, 4) + + # Plot each clique highlighted in a separate axes + fig, axs = plt.subplots(3, 4) + axs = axs.ravel() + for clique, ax in zip(cliques, axs): + ig.plot( + ig.VertexCover(g, [clique]), + mark_groups=True, palette=ig.RainbowPalette(), + edge_width=0.5, + target=ax, + ) + + plt.axis('off') + plt.show() + +The plot looks like this: + +.. figure:: ./figures/cliques.png + :alt: A visual representation of the cliques in the graph + :align: center + + Each clique of the graph is highlighted in one of the panels + +Advanced: improving plotting style +---------------------------------- +If you want a little more style, you can color the vertices/edges within each clique to make them stand out: + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + + g = ig.Graph.Famous('Zachary') + cliques = g.cliques(4, 4) + + fig, axs = plt.subplots(3, 4) + axs = axs.ravel() + for clique, ax in zip(cliques, axs): + # Color vertices yellow/red based on whether they are in this clique + g.vs['color'] = 'yellow' + g.vs[clique]['color'] = 'red' + + # Color edges black/red based on whether they are in this clique + clique_edges = g.es.select(_within=clique) + g.es['color'] = 'black' + clique_edges['color'] = 'red' + # also increase thickness of clique edges + g.es['width'] = 0.3 + clique_edges['width'] = 1 + + ig.plot( + ig.VertexCover(g, [clique]), + mark_groups=True, + palette=ig.RainbowPalette(), + target=ax, + ) + + plt.axis('off') + plt.show() + +Lo and behold: + +.. figure:: ./figures/cliques_with_edges.png + :alt: A visual representation of the cliques in the graph + :align: center + + Each clique of the graph is highlighted in one of the panels, with vertices and edges highlighted as well. + From 2fb246e2ed915c39ea9da02ecc1077667d947f1f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 16 Dec 2021 08:23:09 +1100 Subject: [PATCH 0568/1681] forgot to adapt one of the scripts --- .../visualize_cliques/assets/visualize_cliques_with_edges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py b/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py index faf3b9f11..f677371fc 100644 --- a/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py +++ b/doc/source/tutorials/visualize_cliques/assets/visualize_cliques_with_edges.py @@ -12,10 +12,10 @@ g.vs[clique]['color'] = 'red' # Color edges black/red based on whether they are in this clique - # also increase thickness of clique edges clique_edges = g.es.select(_within=clique) g.es['color'] = 'black' clique_edges['color'] = 'red' + # also increase thickness of clique edges g.es['width'] = 0.3 clique_edges['width'] = 1 From a53bfdc49ad6ff817fe96dce8b31ab9bb9c7adc5 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Thu, 16 Dec 2021 17:27:23 +1100 Subject: [PATCH 0569/1681] fix: few grammar plus edge width --- .../tutorials/erdos_renyi/erdos_renyi.rst | 8 ++--- .../shortest_paths/assets/shortest_path.py | 2 +- .../shortest_paths/figures/shortest_path.png | Bin 27685 -> 29371 bytes .../shortest_paths/shortest_paths.rst | 33 ++++++++++++++++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index 116551bf3..d38c299d6 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -82,16 +82,16 @@ The received output is: IGRAPH U--- 20 35 -- .. figure:: ./figures/erdos_renyi_p.png - :alt: The visual representation of a randomly generated Erdos Renyi graph + :alt: The visual representation of a randomly generated Erdős-Rényi graph :align: center - Erdos Renyi Random Graphs With Probability ``p`` = 0.2 + Erdős-Rényi Random Graphs With Probability ``p`` = 0.2 .. figure:: ./figures/erdos_renyi_m.png - :alt: The second visual representation of a randomly generated Erdos Renyi graph + :alt: The second visual representation of a randomly generated Erdős-Rényi graph :align: center - Erdos Renyi Random Graphs With Number of Edges ``m`` = 35 + Erdős-Rényi Random Graphs With Number of Edges ``m`` = 35 .. note:: diff --git a/doc/source/tutorials/shortest_paths/assets/shortest_path.py b/doc/source/tutorials/shortest_paths/assets/shortest_path.py index e79b63315..9144c7881 100644 --- a/doc/source/tutorials/shortest_paths/assets/shortest_path.py +++ b/doc/source/tutorials/shortest_paths/assets/shortest_path.py @@ -12,7 +12,7 @@ if len(results[0]) > 0: # The distance is the number of vertices in the shortest path minus one. - print("Shortest distance is: ", len(results[0])-1) + print("Shortest distance is: ", len(results[0]) - 1) else: print("End node could not be reached!") diff --git a/doc/source/tutorials/shortest_paths/figures/shortest_path.png b/doc/source/tutorials/shortest_paths/figures/shortest_path.png index 7c77c04e0cecda3ffc3d1b06626701efffed84b5..b77a93f2200756688757df66162235802ccc2d82 100644 GIT binary patch literal 29371 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 N?AO%Oc&ug}@GtrOV|f4o diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index 1cff2be95..1f7103a57 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -65,9 +65,7 @@ The output of these these two shortest paths are: :alt: The visual representation of a weighted network for finding shortest paths :align: center - The Graph `g` - -.. TODO: Add in edge weights as labels when possible! Matplotlib does not support displaying edge weights (and the develop branch implementation is bugged). + The graph `g` with the shortest path from vertex 0 to vertex 5 highlighted. .. note:: @@ -76,4 +74,33 @@ The output of these these two shortest paths are: - If you're interested in finding *all* shortest paths, take a look at |get_all_shortest_paths|_. +In case you are wondering how the visualization figure was done, here's the code: + +.. code-block:: python + import igraph as ig + import matplotlib.pyplot as plt + + # Find the shortest path on an unweighted graph + g = ig.Graph( + 6, + [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] + ) + g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] + + # g.get_shortest_paths() returns a list of edge ID paths + results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] + + fig, ax = plt.subplots() + g.es['width'] = 1 + for edge in g.es: + if set([edge.source, edge.target]) in [set([0, 1]), set([1, 3]), set([3, 5])]: + edge['width'] = 4 + ig.plot( + g, + target=ax, + layout='circle', + vertex_color='steelblue', + vertex_label=['0', '1', '2', '3', '4', '5'], + edge_width=g.es['width'], + ) From fbd654ebbf66b6639f7d3f48f0214953756e6604 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 17 Dec 2021 19:40:21 +1100 Subject: [PATCH 0570/1681] Tutorial on online user actions --- doc/source/gallery.rst | 2 + .../assets/online_users.py | 63 ++++++++++ .../figures/online_users.png | Bin 0 -> 48823 bytes .../figures/online_users_simple.png | Bin 0 -> 47195 bytes .../online_user_actions.rst | 113 ++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 doc/source/tutorials/online_user_actions/assets/online_users.py create mode 100644 doc/source/tutorials/online_user_actions/figures/online_users.png create mode 100644 doc/source/tutorials/online_user_actions/figures/online_users_simple.png create mode 100644 doc/source/tutorials/online_user_actions/online_user_actions.rst diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index 16cb26f8c..d29d7d2c4 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -16,6 +16,7 @@ This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-maxflow` - :ref:`tutorials-shortest-paths` - :ref:`tutorials-cliques` + - :ref:`tutorials-online-user-actions` .. toctree:: @@ -30,3 +31,4 @@ This page contains short examples showcasing the functionality of |igraph|: tutorials/ring_animation/ring_animation tutorials/shortest_paths/shortest_paths tutorials/visualize_cliques/visualize_cliques + tutorials/online_user_actions/online_user_actions diff --git a/doc/source/tutorials/online_user_actions/assets/online_users.py b/doc/source/tutorials/online_user_actions/assets/online_users.py new file mode 100644 index 000000000..12dec7210 --- /dev/null +++ b/doc/source/tutorials/online_user_actions/assets/online_users.py @@ -0,0 +1,63 @@ +import igraph as ig +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +# User data (usually would come with time stamp) +action_dataframe = pd.DataFrame([ + ['dsj3239asadsa3', 'createPage', 'greatProject'], + ['2r09ej221sk2k5', 'editPage', 'greatProject'], + ['dsj3239asadsa3', 'editPage', 'greatProject'], + ['789dsadafj32jj', 'editPage', 'greatProject'], + ['oi32ncwosap399', 'editPage', 'greatProject'], + ['4r4320dkqpdokk', 'createPage', 'miniProject'], + ['320eljl3lk3239', 'editPage', 'miniProject'], + ['dsj3239asadsa3', 'editPage', 'miniProject'], + ['3203ejew332323', 'createPage', 'private'], + ['3203ejew332323', 'editPage', 'private'], + ['40m11919332msa', 'createPage', 'private2'], + ['40m11919332msa', 'editPage', 'private2'], + ['dsj3239asadsa3', 'createPage', 'anotherGreatProject'], + ['2r09ej221sk2k5', 'editPage', 'anotherGreatProject'], + ], + columns=['userid', 'action', 'project'], +) + +# Make a graph with vertices as users, and edges connecting two users who +# worked on the same project +users = action_dataframe['userid'].unique() +adjacency_matrix = pd.DataFrame( + np.zeros((len(users), len(users)), np.int32), + index=users, + columns=users, +) +for project, project_data in action_dataframe.groupby('project'): + project_users = project_data['userid'].values + for i1, user1 in enumerate(project_users): + for user2 in project_users[:i1]: + adjacency_matrix.at[user1, user2] += 1 + +g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode='plus') + +# Plot the graph +# Make a layout first +layout = g.layout('circle') + +# Make vertex size based on their closeness to other vertices +vertex_size = g.closeness() +vertex_size = [0.5 * v**2 if not np.isnan(v) else 0.05 for v in vertex_size] + +# Make mpl axes +fig, ax = plt.subplots() + +# Plot graph in that axes +ig.plot( + g, + target=ax, + layout=layout, + vertex_label=g.vs['name'], + vertex_color="lightblue", + vertex_size=vertex_size, + edge_width=g.es["weight"], +) +plt.show() diff --git a/doc/source/tutorials/online_user_actions/figures/online_users.png b/doc/source/tutorials/online_user_actions/figures/online_users.png new file mode 100644 index 0000000000000000000000000000000000000000..2f0a09d6835654ee7b65cf4c021d4b47d78bb880 GIT binary patch literal 48823 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

    569=ny6zm{M3m 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 NJkJFlhuuO`{toAlnvVbg literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/online_user_actions/figures/online_users_simple.png b/doc/source/tutorials/online_user_actions/figures/online_users_simple.png new file mode 100644 index 0000000000000000000000000000000000000000..eee2ee4e8f237edfae277bdf6404e29d16ce0aac GIT binary patch literal 47195 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 literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/online_user_actions/online_user_actions.rst b/doc/source/tutorials/online_user_actions/online_user_actions.rst new file mode 100644 index 000000000..d938de141 --- /dev/null +++ b/doc/source/tutorials/online_user_actions/online_user_actions.rst @@ -0,0 +1,113 @@ +.. include:: ../../include/global.rst + +.. _tutorials-online-user-actions: + +=================== +Online user actions +=================== + +This example reproduces a typical data science situation in an internet company. We start from a pandas DataFrame with online user actions, for instance for an online text editor: the user can create a page, edit it, or delete it. We want to construct and visualize a graph of the users highlighting collaborations on the same page/project. + +.. code-block:: python + + import igraph as ig + import numpy as np + import pandas as pd + import matplotlib.pyplot as plt + + # User data (usually would come with time stamp) + action_dataframe = pd.DataFrame([ + ['dsj3239asadsa3', 'createPage', 'greatProject'], + ['2r09ej221sk2k5', 'editPage', 'greatProject'], + ['dsj3239asadsa3', 'editPage', 'greatProject'], + ['789dsadafj32jj', 'editPage', 'greatProject'], + ['oi32ncwosap399', 'editPage', 'greatProject'], + ['4r4320dkqpdokk', 'createPage', 'miniProject'], + ['320eljl3lk3239', 'editPage', 'miniProject'], + ['dsj3239asadsa3', 'editPage', 'miniProject'], + ['3203ejew332323', 'createPage', 'private'], + ['3203ejew332323', 'editPage', 'private'], + ['40m11919332msa', 'createPage', 'private2'], + ['40m11919332msa', 'editPage', 'private2'], + ['dsj3239asadsa3', 'createPage', 'anotherGreatProject'], + ['2r09ej221sk2k5', 'editPage', 'anotherGreatProject'], + ], + columns=['userid', 'action', 'project'], + ) + +This block just introduces the toy data: a DataFrame with three columns, namely the user id, the action, and the page or project that was being actioned upon. Now we need to check when two users worked on the same page. We choose to use a weighted adjacency matrix for this, i.e. a table with rows and columns indexes by the users that has nonzero entries whenever folks collaborate. First, let's get the users and prepare an empty matrix: + +.. code-block:: python + + users = action_dataframe['userid'].unique() + adjacency_matrix = pd.DataFrame( + np.zeros((len(users), len(users)), np.int32), + index=users, + columns=users, + ) + +Then, let's iterate over all projects one by one, and add all collaborations: + +.. code-block:: python + + for project, project_data in action_dataframe.groupby('project'): + project_users = project_data['userid'].values + for i1, user1 in enumerate(project_users): + for user2 in project_users[:i1]: + adjacency_matrix.at[user1, user2] += 1 + +There are many ways to achieve the above matrix, so don't be surprised if you came up with another algorithm ;-) + +Now it's time to make the graph: + +.. code-block:: python + + g = ig.Graph.Weighted_Adjacency(adjacency_matrix, mode='plus') + +And finally, let's plot a layout of the graph, for instance a circle: + +.. code-block:: python + + # Make a layout first + layout = g.layout('circle') + + # Make vertex size based on their closeness to other vertices + vertex_size = g.closeness() + vertex_size = [0.5 * v**2 if not np.isnan(v) else 0.05 for v in vertex_size] + + # Make mpl axes + fig, ax = plt.subplots() + + # Plot graph in that axes + ig.plot( + g, + target=ax, + layout=layout, + vertex_label=g.vs['name'], + vertex_color="lightblue", + vertex_size=vertex_size, + edge_width=g.es["weight"], + ) + plt.show() + +We added a few fancy features to this plot to show off igraph's capabilities. The result is shown below. + +.. figure:: ./figures/online_users.png + :alt: A visual representation of the collaboratoin graph + :align: center + + The collaboration graph: thicker edges mean multiple collaborations, and larger vertices indicate users with higher closeness to the rest of the network. + +Loops indicate "self-collaborations", which are not very meaningful. To filter out loops without losing the edge weights, we can use: + +.. code-block:: python + + g = g.simplify(combine_edges='first') + +and then repeat the plotting code verbatim. The result is shown below. + +.. figure:: ./figures/online_users_simple.png + :alt: A visual representation of the collaboratoin graph + :align: center + + Simplified graph after loops are filtered out. From 9ea92c3854ef0248da486267c8c5ec09a97f5a41 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Fri, 17 Dec 2021 20:06:21 +1100 Subject: [PATCH 0571/1681] Tutorial on community visualization from usability benchmarks --- doc/source/gallery.rst | 2 + .../assets/visualize_communities.py | 48 ++++++++++ .../figures/visualize_communities.png | Bin 0 -> 57542 bytes .../visualize_communities.rst | 83 ++++++++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 doc/source/tutorials/visualize_communities/assets/visualize_communities.py create mode 100644 doc/source/tutorials/visualize_communities/figures/visualize_communities.png create mode 100644 doc/source/tutorials/visualize_communities/visualize_communities.rst diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index d29d7d2c4..45f49e62b 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -17,6 +17,7 @@ This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-shortest-paths` - :ref:`tutorials-cliques` - :ref:`tutorials-online-user-actions` + - :ref:`tutorials-visualize-communities` .. toctree:: @@ -32,3 +33,4 @@ This page contains short examples showcasing the functionality of |igraph|: tutorials/shortest_paths/shortest_paths tutorials/visualize_cliques/visualize_cliques tutorials/online_user_actions/online_user_actions + tutorials/visualize_communities/visualize_communities diff --git a/doc/source/tutorials/visualize_communities/assets/visualize_communities.py b/doc/source/tutorials/visualize_communities/assets/visualize_communities.py new file mode 100644 index 000000000..614bb612d --- /dev/null +++ b/doc/source/tutorials/visualize_communities/assets/visualize_communities.py @@ -0,0 +1,48 @@ +import igraph as ig +import matplotlib.pyplot as plt + +g = ig.Graph.Famous("Zachary") + +# Use edge betweenness to detect communities +# and covert into a VertexClustering +communities = g.community_edge_betweenness() +communities = communities.as_clustering() + +# Color each vertex and edge based on its community membership +num_communities = len(communities) +palette = ig.RainbowPalette(n=num_communities) +for i, community in enumerate(communities): + g.vs[community]["color"] = i + community_edges = g.es.select(_within=community) + community_edges["color"] = i + + +# Plot with only vertex and edge coloring +fig, ax = plt.subplots() +ig.plot( + communities, + palette=palette, + edge_width=1, + target=ax, + vertex_size=0.3, +) + +# Create a custom color legend +legend_handles = [] +for i in range(num_communities): + handle = ax.scatter( + [], [], + s=100, + facecolor=palette.get(i), + edgecolor="k", + label=i, + ) + legend_handles.append(handle) +ax.legend( + handles=legend_handles, + title='Community:', + bbox_to_anchor=(0, 1.0), + bbox_transform=ax.transAxes, +) + +plt.show() diff --git a/doc/source/tutorials/visualize_communities/figures/visualize_communities.png b/doc/source/tutorials/visualize_communities/figures/visualize_communities.png new file mode 100644 index 0000000000000000000000000000000000000000..209720200914ce3df784b6348a62e62d6e11ae1b GIT binary patch literal 57542 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

    _ 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 Date: Sat, 18 Dec 2021 00:30:13 +1100 Subject: [PATCH 0572/1681] Corrected shortest path visualisation Added edge labels and fixed the path to reflect the one found in g.get_shortest_path() --- .../shortest_paths/figures/shortest_path.png | Bin 29371 -> 30185 bytes .../shortest_paths/shortest_paths.rst | 24 ++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/doc/source/tutorials/shortest_paths/figures/shortest_path.png b/doc/source/tutorials/shortest_paths/figures/shortest_path.png index b77a93f2200756688757df66162235802ccc2d82..cbfacff6e3fc6a1b30caf954375ee8dc6ef93927 100644 GIT binary patch literal 30185 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 E0UiHjHvj+t literal 29371 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 Date: Sat, 18 Dec 2021 00:50:13 +1100 Subject: [PATCH 0573/1681] Add shortest_path_visualisation.py to assets --- .../assets/shortest_path_visualisation.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py diff --git a/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py b/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py new file mode 100644 index 000000000..485326035 --- /dev/null +++ b/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py @@ -0,0 +1,31 @@ +import igraph as ig +import matplotlib.pyplot as plt + +import igraph as ig +import matplotlib.pyplot as plt + +# Construct the graph +g = ig.Graph( + 6, + [(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 5), (4, 5)] +) +g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] + +# Get a shortest path along edges +results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] + +# Plot graph +g.es['width'] = 0.5 +g.es[results[0]]['width'] = 2 + +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout='circle', + vertex_color='steelblue', + vertex_label=range(g.vcount()), + edge_width=g.es['width'], + edge_label=g.es["weight"] +) +fig.savefig('../figures/shortest_path.png', dpi=100) From 533d51277864813487a6a4c7a6df127c06228c3a Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 18 Dec 2021 01:31:32 +1100 Subject: [PATCH 0574/1681] Add "Visualizing Betweenness" tutorial Documents how to visualize both vertex and edge betweenness --- doc/source/gallery.rst | 2 + .../betweenness/assets/betweenness.py | 39 +++++++++++ .../tutorials/betweenness/betweenness.rst | 63 ++++++++++++++++++ .../betweenness/figures/betweenness.png | Bin 0 -> 380066 bytes 4 files changed, 104 insertions(+) create mode 100644 doc/source/tutorials/betweenness/assets/betweenness.py create mode 100644 doc/source/tutorials/betweenness/betweenness.rst create mode 100644 doc/source/tutorials/betweenness/figures/betweenness.png diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index 45f49e62b..e669406cc 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -9,6 +9,7 @@ Gallery This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-quickstart` + - :ref:`tutorials-betweenness` - :ref:`tutorials-bipartite-matching` - :ref:`tutorials-bipartite-matching-maxflow` - :ref:`tutorials-random` @@ -25,6 +26,7 @@ This page contains short examples showcasing the functionality of |igraph|: :hidden: tutorials/quickstart/quickstart + tutorials/betweenness/betweenness tutorials/bipartite_matching/bipartite_matching tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow tutorials/erdos_renyi/erdos_renyi diff --git a/doc/source/tutorials/betweenness/assets/betweenness.py b/doc/source/tutorials/betweenness/assets/betweenness.py new file mode 100644 index 000000000..af8c749ad --- /dev/null +++ b/doc/source/tutorials/betweenness/assets/betweenness.py @@ -0,0 +1,39 @@ +import igraph as ig +import matplotlib.pyplot as plt +import math +import random + +# Generate graph +random.seed(1) +g = ig.Graph.Barabasi(n=200, m=2) + +# Calculate vertex betweenness and scale it to be between 0.0 and 1.0 +vertex_betweenness = g.betweenness() +vertex_betweenness = [math.pow(i, 1/3) for i in vertex_betweenness] # scale values so transition is smoother +min_vertex_betweenness = min(vertex_betweenness) +max_vertex_betweenness = max(vertex_betweenness) +vertex_betweenness = [(i - min_vertex_betweenness) / (max_vertex_betweenness - min_vertex_betweenness) for i in vertex_betweenness] + +# Calculate edge betweenness and scale it to be between 0.0 and 1.0 +edge_betweenness = g.edge_betweenness() +edge_betweenness = [math.pow(i, 1/2) for i in edge_betweenness] # scale values so transition is smoother +min_edge_betweenness = min(edge_betweenness) +max_edge_betweenness = max(edge_betweenness) +edge_betweenness = [(i - min_edge_betweenness) / (max_edge_betweenness - min_edge_betweenness) for i in edge_betweenness] + +# Plot the graph +fig, ax = plt.subplots() +ig.plot( + g, + target=ax, + layout="fruchterman_reingold", + palette=ig.GradientPalette("white", "midnightblue"), + vertex_color=[int(betweenness * 255) for betweenness in vertex_betweenness], # colors are integers between 0 and 255 + edge_color=[int(betweenness * 255) for betweenness in edge_betweenness], + vertex_size=[betweenness*0.5+0.1 for betweenness in vertex_betweenness], # vertex_size is between 0.1 and 0.6 + edge_width=[betweenness*0.5+0.5 for betweenness in edge_betweenness], # edge_width is between 0.5 and 1 + vertex_frame_width=0.2, +) +plt.show() + +# fig.savefig("../figures/betweenness.png", dpi=200) diff --git a/doc/source/tutorials/betweenness/betweenness.rst b/doc/source/tutorials/betweenness/betweenness.rst new file mode 100644 index 000000000..0edc7fd36 --- /dev/null +++ b/doc/source/tutorials/betweenness/betweenness.rst @@ -0,0 +1,63 @@ +.. include:: ../../include/global.rst + +.. _tutorials-betweenness: + +======================= +Visualizing Betweenness +======================= + +.. _betweenness: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#betweenness +.. |betweenness| replace:: :meth:`betweenness` +.. _edge_betweenness: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#edge_betweenness +.. |edge_betweenness| replace:: :meth:`edge_betweenness` + +This example will demonstrate how to visualize both vertex and edge betweenness with a custom defined color palette. We use the methods |betweenness|_ and |edge_betweenness|_ respectively. + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + import math + import random + + # Generate graph + random.seed(1) + g = ig.Graph.Barabasi(n=200, m=2) + + # Calculate vertex betweenness and scale it to be between 0.0 and 1.0 + vertex_betweenness = g.betweenness() + vertex_betweenness = [math.pow(i, 1/3) for i in vertex_betweenness] + min_vertex_betweenness = min(vertex_betweenness) + max_vertex_betweenness = max(vertex_betweenness) + vertex_betweenness = [(i - min_vertex_betweenness) / (max_vertex_betweenness - min_vertex_betweenness) for i in vertex_betweenness] + + # Calculate edge betweenness and scale it to be between 0.0 and 1.0 + edge_betweenness = g.edge_betweenness() + edge_betweenness = [math.pow(i, 1/2) for i in edge_betweenness] + min_edge_betweenness = min(edge_betweenness) + max_edge_betweenness = max(edge_betweenness) + edge_betweenness = [(i - min_edge_betweenness) / (max_edge_betweenness - min_edge_betweenness) for i in edge_betweenness] + + # Plot the graph + fig, ax = plt.subplots() + ig.plot( + g, + target=ax, + layout="fruchterman_reingold", + palette=ig.GradientPalette("white", "midnightblue"), # define a new color palette + vertex_color=[int(betweenness * 255) for betweenness in vertex_betweenness], # colors are integers between 0 and 255 + edge_color=[int(betweenness * 255) for betweenness in edge_betweenness], + vertex_size=[betweenness*0.5+0.1 for betweenness in vertex_betweenness], # vertex_size is between 0.1 and 0.6 + edge_width=[betweenness*0.5+0.5 for betweenness in edge_betweenness], # edge_width is between 0.5 and 1 + vertex_frame_width=0.2, + ) + plt.show() + +Note that we scale the betweennesses for the vertices and edges by the cube root and square root respectively. The choice of scaling is arbitrary, but is used to give a smoother, more linear transition in the sizes and colors of nodes and edges. The final output graph is here: + +.. figure:: ./figures/betweenness.png + :alt: A graph visualizing the betweenness of each vertex and edge. + :align: center + + A graph visualizing edge betweenness. White indicates a low betweenness centrality, whereas dark blue indicates a high betweenness centrality. + diff --git a/doc/source/tutorials/betweenness/figures/betweenness.png b/doc/source/tutorials/betweenness/figures/betweenness.png new file mode 100644 index 0000000000000000000000000000000000000000..1d61677bcda8f5b94013038b2cbe2c016dba5779 GIT binary patch literal 380066 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

  • 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 z%0D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 z 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 L<60z5yWsx?ZXCTT literal 0 HcmV?d00001 From d1977f071d2986179e7d7a69199b806244c5f4b6 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 18 Dec 2021 11:17:53 +1100 Subject: [PATCH 0575/1681] Edge label background box and alignment to edge --- doc/source/tutorial.rst | 19 ++++++++++++++-- src/igraph/drawing/baseclasses.py | 31 ++++++++++++++++++++------ src/igraph/drawing/matplotlib/edge.py | 2 ++ src/igraph/drawing/matplotlib/graph.py | 22 ++++++++++++++++-- 4 files changed, 63 insertions(+), 11 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index bd961f4ec..1c8148fed 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -113,9 +113,13 @@ user-friendly output, we can try to print the graph using Python's This summary consists of `IGRAPH`, followed by a four-character long code, the number of vertices, the number of edges, two dashes (`--`) and the name of the graph (i.e. the contents of the `name` attribute, if any) This is not too exciting so far; a graph with no vertices and no edges is not really useful -for us. Let's add some vertices first! +for us. Of course, there are dozens of ways to create a graph with vertices and edges, depending on your exact situation. In general, if you know the number of vertices and already have a list of edges connecting them, you can just use:: -:: + >>> g = ig.Graph(n=10, edges=[[0, 1], [0, 5]]) + +This creates a graph with 10 vertices, numbered 0 to 9, and two edges connecting vertex 0 with vertex 1, and vertex 0 (again) with vertex 5. See :ref:`generation` for a detailed overview of all the possible ways to create graphs in |igraph|. + +A less common situation, which we'll follow here as an example, is to add vertices and edges to an existing graph, which in this case is the empty graph. First, to add vertices:: >>> g.add_vertices(3) @@ -918,6 +922,17 @@ Attribute name Keyword argument Purpose graph is directed, relative to 10 pixels. --------------- ---------------------- ------------------------------------------ ``width`` ``edge_width`` Width of the edge in pixels +--------------- ---------------------- ------------------------------------------ +``label`` ``edge_label`` If specified, it adds a label to the edge. +--------------- ---------------------- ------------------------------------------ +``background`` ``edge_background`` If specified, it adds a rectangular box + around the edge label, of the specified + color (matplotlib only). +--------------- ---------------------- ------------------------------------------ +``align_label`` ``edge_align_label`` If True, rotate the edge label such that + it aligns with the edge direction. Labels + that would be upside-down are flipped + (matplotlib only). =============== ====================== ========================================== diff --git a/src/igraph/drawing/baseclasses.py b/src/igraph/drawing/baseclasses.py index 720404ac9..6a983225a 100644 --- a/src/igraph/drawing/baseclasses.py +++ b/src/igraph/drawing/baseclasses.py @@ -121,21 +121,21 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): raise NotImplementedError def get_label_position(self, edge, src_vertex, dest_vertex): - """Returns the position where the label of an edge should be drawn. The + """returns the position where the label of an edge should be drawn. the default implementation returns the midpoint of the edge and an alignment that tries to avoid overlapping the label with the edge. - @param edge: the edge to be drawn. Visual properties of the edge + @param edge: the edge to be drawn. visual properties of the edge are defined by the attributes of this object. - @param src_vertex: the source vertex. Visual properties are given + @param src_vertex: the source vertex. visual properties are given again as attributes. - @param dest_vertex: the target vertex. Visual properties are given + @param dest_vertex: the target vertex. visual properties are given again as attributes. @return: a tuple containing two more tuples: the desired position of the label and the desired alignment of the label, where the position is - given as C{(x, y)} and the alignment is given as C{(horizontal, vertical)}. - Members of the alignment tuple are taken from constants in the - L{TextAlignment} class. + given as c{(x, y)} and the alignment is given as c{(horizontal, vertical)}. + members of the alignment tuple are taken from constants in the + l{textalignment} class. """ # TODO: curved edges don't play terribly well with this function, # we could try to get the mid point of the actual curved arrow @@ -192,6 +192,23 @@ def get_label_position(self, edge, src_vertex, dest_vertex): return pos, (halign, valign) + def get_label_rotation(self, edge, src_vertex, dest_vertex): + """Get the rotation angle of the label to align with the edge. + + @param edge: the edge to be drawn. visual properties of the edge + are defined by the attributes of this object. + @param src_vertex: the source vertex. visual properties are given + again as attributes. + @param dest_vertex: the target vertex. visual properties are given + again as attributes. + @return: a float with the desired angle, in degrees (out of 360). + """ + (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position + rotation = (360 + 180. / pi * atan2(y2 - y1, x2 - x1)) % 360 + # Try to keep text on its head + if 90 < rotation <= 270: + rotation = (180 + rotation) % 360 + return rotation ##################################################################### diff --git a/src/igraph/drawing/matplotlib/edge.py b/src/igraph/drawing/matplotlib/edge.py index 1e13e3cc3..adbdfab89 100644 --- a/src/igraph/drawing/matplotlib/edge.py +++ b/src/igraph/drawing/matplotlib/edge.py @@ -49,6 +49,8 @@ class VisualEdgeBuilder(AttributeCollectorBase): label_size = 12.0 font = "sans-serif" width = 2.0 + background = None + align_label = False return VisualEdgeBuilder diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py index 5037e4f2d..7dc824b8a 100644 --- a/src/igraph/drawing/matplotlib/graph.py +++ b/src/igraph/drawing/matplotlib/graph.py @@ -295,14 +295,32 @@ def draw(self, graph, *args, **kwds): dest_vertex, ) + text_kwargs = {} + text_kwargs['ha'] = halign.value + text_kwargs['va'] = halign.value + + if visual_edge.background is not None: + text_kwargs['bbox'] = dict( + facecolor=visual_edge.background, + edgecolor='none', + ) + text_kwargs['ha'] = 'center' + text_kwargs['va'] = 'center' + + if visual_edge.align_label: + # Rotate the text to align with the edge + rotation = edge_drawer.get_label_rotation( + visual_edge, src_vertex, dest_vertex, + ) + text_kwargs['rotation'] = rotation + ax.text( x, y, label, fontsize=visual_edge.label_size, color=visual_edge.label_color, - ha=halign.value, - va=valign.value, + **text_kwargs, # TODO: offset, etc. ) From 3801725ee9bbc216d02368f8637a265cbc0431c2 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 18 Dec 2021 00:16:41 +1100 Subject: [PATCH 0576/1681] Add tutorial for configuration and visual style --- doc/source/gallery.rst | 8 ++- .../configuration/assets/configuration.py | 25 +++++++ .../tutorials/configuration/configuration.rst | 62 ++++++++++++++++++ .../configuration/figures/configuration.png | Bin 0 -> 44598 bytes .../visual_style/assets/visual_style.py | 30 +++++++++ .../visual_style/figures/visual_style.png | Bin 0 -> 63457 bytes .../tutorials/visual_style/visual_style.rst | 52 +++++++++++++++ 7 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 doc/source/tutorials/configuration/assets/configuration.py create mode 100644 doc/source/tutorials/configuration/configuration.rst create mode 100644 doc/source/tutorials/configuration/figures/configuration.png create mode 100644 doc/source/tutorials/visual_style/assets/visual_style.py create mode 100644 doc/source/tutorials/visual_style/figures/visual_style.png create mode 100644 doc/source/tutorials/visual_style/visual_style.rst diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst index 45f49e62b..78d8b35c9 100644 --- a/doc/source/gallery.rst +++ b/doc/source/gallery.rst @@ -2,9 +2,9 @@ .. _gallery: -======== +======= Gallery -======== +======= This page contains short examples showcasing the functionality of |igraph|: @@ -16,6 +16,8 @@ This page contains short examples showcasing the functionality of |igraph|: - :ref:`tutorials-maxflow` - :ref:`tutorials-shortest-paths` - :ref:`tutorials-cliques` + - :ref:`tutorials-configuration` + - :ref:`tutorials-visual-style` - :ref:`tutorials-online-user-actions` - :ref:`tutorials-visualize-communities` @@ -32,5 +34,7 @@ This page contains short examples showcasing the functionality of |igraph|: tutorials/ring_animation/ring_animation tutorials/shortest_paths/shortest_paths tutorials/visualize_cliques/visualize_cliques + tutorials/configuration/configuration + tutorials/visual_style/visual_style tutorials/online_user_actions/online_user_actions tutorials/visualize_communities/visualize_communities diff --git a/doc/source/tutorials/configuration/assets/configuration.py b/doc/source/tutorials/configuration/assets/configuration.py new file mode 100644 index 000000000..ad8424221 --- /dev/null +++ b/doc/source/tutorials/configuration/assets/configuration.py @@ -0,0 +1,25 @@ +import igraph as ig +import matplotlib.pyplot as plt +import math +import random + +# Get the configuration instance +config = ig.Configuration().instance() + +# Set configuration variables +config["general.verbose"] = True +config["plotting.backend"] = "matplotlib" +config["plotting.layout"] = "fruchterman_reingold" +config["plotting.palette"] = "heat" + +# Generate a graph +random.seed(1) +g = ig.Graph.Barabasi(n=100, m=1) + +# Calculate colors between 0-255 for all nodes +betweenness = g.betweenness() +colors = [math.floor(i * 255 / max(betweenness)) for i in betweenness] + +# Plot the graph +ig.plot(g, vertex_color=colors, vertex_size=1, edge_width=0.3) +plt.show() diff --git a/doc/source/tutorials/configuration/configuration.rst b/doc/source/tutorials/configuration/configuration.rst new file mode 100644 index 000000000..31b423136 --- /dev/null +++ b/doc/source/tutorials/configuration/configuration.rst @@ -0,0 +1,62 @@ +.. include:: ../../include/global.rst + +.. _tutorials-configuration: + +====================== +Configuration Instance +====================== + +This example shows how to use |igraph|'s `configuration instance `_ to set default |igraph| settings. This is useful for setting global settings so that they don't need to be explicitly stated at the beginning of every |igraph| project you work on. + +First we define the default plotting backend, layout, and color palette, and save them. By default, ``config.save()`` will save files to ``~/.igraphrc`` on Linux and Max OS X systems, or in ``C:\Documents and Settings\username\.igraphrc`` for Windows systems. + +.. code-block:: python + + import igraph as ig + + # Get the configuration instance + config = ig.Configuration().instance() + + # Set configuration variables + config["general.verbose"] = True + config["plotting.backend"] = "matplotlib" + config["plotting.layout"] = "fruchterman_reingold" + config["plotting.palette"] = "rainbow" + + # Save configuration to ~/.igraphrc + config.save() + +This script only needs to be run once, and can then be deleted. Afterwards any time you use |igraph|, it will read the config from the saved file and use them as the defaults. For example: + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + import random + + # Generate a graph + random.seed(1) + g = ig.Graph.Barabasi(n=100, m=1) + + # Calculate a color value between 0-200 for all nodes + betweenness = g.betweenness() + colors = [int(i * 200 / max(betweenness)) for i in betweenness] + + # Plot the graph + ig.plot(g, vertex_color=colors, vertex_size=1, edge_width=0.3) + plt.show() + +Note that we do not never explicitly state the backend, layout or palette, yet the final plots look like this: + +.. figure:: ./figures/configuration.png + :alt: A 100 node graph colored to show betweenness + :align: center + + Graph colored based on each node's betweenness centrality measure. + +The full list of config settings can be found `here `_. + +.. note:: + + - Note that you can specify your own location by passing in a filepath to ``config.save``, e.g. ``config.save("./path/to/config/file")``. You can then load it later using ``ig.Configuration().instance().load("./path/to/config/file")`` + - If you want an efficient way to set the visual style between individual graphs (such as vertex sizes, colors, layout etc.) check out :ref:`tutorials-visual-style`. diff --git a/doc/source/tutorials/configuration/figures/configuration.png b/doc/source/tutorials/configuration/figures/configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..73339f27684a14a68d01adc91aa37f2d65fff518 GIT binary patch literal 44598 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 d_`_. A quick example on how to use it can be found here: :ref:`tutorials-configuration` From 38978c2ec5841f7138c6a82c338a9fbec1d33058 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 18 Dec 2021 13:08:35 +1100 Subject: [PATCH 0577/1681] Switch to `ig.config` in tutorial --- .../configuration/assets/configuration.py | 25 ------------------- .../configuration/assets/configuration1.py | 12 +++++++++ .../configuration/assets/configuration2.py | 15 +++++++++++ .../tutorials/configuration/configuration.rst | 17 ++++++------- 4 files changed, 34 insertions(+), 35 deletions(-) delete mode 100644 doc/source/tutorials/configuration/assets/configuration.py create mode 100644 doc/source/tutorials/configuration/assets/configuration1.py create mode 100644 doc/source/tutorials/configuration/assets/configuration2.py diff --git a/doc/source/tutorials/configuration/assets/configuration.py b/doc/source/tutorials/configuration/assets/configuration.py deleted file mode 100644 index ad8424221..000000000 --- a/doc/source/tutorials/configuration/assets/configuration.py +++ /dev/null @@ -1,25 +0,0 @@ -import igraph as ig -import matplotlib.pyplot as plt -import math -import random - -# Get the configuration instance -config = ig.Configuration().instance() - -# Set configuration variables -config["general.verbose"] = True -config["plotting.backend"] = "matplotlib" -config["plotting.layout"] = "fruchterman_reingold" -config["plotting.palette"] = "heat" - -# Generate a graph -random.seed(1) -g = ig.Graph.Barabasi(n=100, m=1) - -# Calculate colors between 0-255 for all nodes -betweenness = g.betweenness() -colors = [math.floor(i * 255 / max(betweenness)) for i in betweenness] - -# Plot the graph -ig.plot(g, vertex_color=colors, vertex_size=1, edge_width=0.3) -plt.show() diff --git a/doc/source/tutorials/configuration/assets/configuration1.py b/doc/source/tutorials/configuration/assets/configuration1.py new file mode 100644 index 000000000..a10d4e601 --- /dev/null +++ b/doc/source/tutorials/configuration/assets/configuration1.py @@ -0,0 +1,12 @@ +import igraph as ig + +# Set configuration variables +ig.config["general.verbose"] = True +ig.config["plotting.backend"] = "matplotlib" +ig.config["plotting.layout"] = "fruchterman_reingold" +ig.config["plotting.palette"] = "rainbow" + +# Save configuration to ~/.igraphrc +ig.config.save() + + diff --git a/doc/source/tutorials/configuration/assets/configuration2.py b/doc/source/tutorials/configuration/assets/configuration2.py new file mode 100644 index 000000000..7c32aec8a --- /dev/null +++ b/doc/source/tutorials/configuration/assets/configuration2.py @@ -0,0 +1,15 @@ +import igraph as ig +import matplotlib.pyplot as plt +import random + +# Generate a graph +random.seed(1) +g = ig.Graph.Barabasi(n=100, m=1) + +# Calculate a color value between 0-200 for all nodes +betweenness = g.betweenness() +colors = [int(i * 200 / max(betweenness)) for i in betweenness] + +# Plot the graph +ig.plot(g, vertex_color=colors, vertex_size=1, edge_width=0.3) +plt.show() diff --git a/doc/source/tutorials/configuration/configuration.rst b/doc/source/tutorials/configuration/configuration.rst index 31b423136..f3c5db155 100644 --- a/doc/source/tutorials/configuration/configuration.rst +++ b/doc/source/tutorials/configuration/configuration.rst @@ -8,23 +8,20 @@ Configuration Instance This example shows how to use |igraph|'s `configuration instance `_ to set default |igraph| settings. This is useful for setting global settings so that they don't need to be explicitly stated at the beginning of every |igraph| project you work on. -First we define the default plotting backend, layout, and color palette, and save them. By default, ``config.save()`` will save files to ``~/.igraphrc`` on Linux and Max OS X systems, or in ``C:\Documents and Settings\username\.igraphrc`` for Windows systems. +First we define the default plotting backend, layout, and color palette, and save them. By default, ``ig.config.save()`` will save files to ``~/.igraphrc`` on Linux and Max OS X systems, or in ``C:\Documents and Settings\username\.igraphrc`` for Windows systems. .. code-block:: python import igraph as ig - # Get the configuration instance - config = ig.Configuration().instance() - # Set configuration variables - config["general.verbose"] = True - config["plotting.backend"] = "matplotlib" - config["plotting.layout"] = "fruchterman_reingold" - config["plotting.palette"] = "rainbow" + ig.config["general.verbose"] = True + ig.config["plotting.backend"] = "matplotlib" + ig.config["plotting.layout"] = "fruchterman_reingold" + ig.config["plotting.palette"] = "rainbow" # Save configuration to ~/.igraphrc - config.save() + ig.config.save() This script only needs to be run once, and can then be deleted. Afterwards any time you use |igraph|, it will read the config from the saved file and use them as the defaults. For example: @@ -58,5 +55,5 @@ The full list of config settings can be found `here Date: Sat, 18 Dec 2021 13:13:52 +1100 Subject: [PATCH 0578/1681] Remove general.verbose from example When running as a library, which is what the example intends to show, general.verbose does nothing. --- doc/source/tutorials/configuration/assets/configuration1.py | 1 - doc/source/tutorials/configuration/configuration.rst | 1 - 2 files changed, 2 deletions(-) diff --git a/doc/source/tutorials/configuration/assets/configuration1.py b/doc/source/tutorials/configuration/assets/configuration1.py index a10d4e601..cd3862e0e 100644 --- a/doc/source/tutorials/configuration/assets/configuration1.py +++ b/doc/source/tutorials/configuration/assets/configuration1.py @@ -1,7 +1,6 @@ import igraph as ig # Set configuration variables -ig.config["general.verbose"] = True ig.config["plotting.backend"] = "matplotlib" ig.config["plotting.layout"] = "fruchterman_reingold" ig.config["plotting.palette"] = "rainbow" diff --git a/doc/source/tutorials/configuration/configuration.rst b/doc/source/tutorials/configuration/configuration.rst index f3c5db155..24d5188f0 100644 --- a/doc/source/tutorials/configuration/configuration.rst +++ b/doc/source/tutorials/configuration/configuration.rst @@ -15,7 +15,6 @@ First we define the default plotting backend, layout, and color palette, and sav import igraph as ig # Set configuration variables - ig.config["general.verbose"] = True ig.config["plotting.backend"] = "matplotlib" ig.config["plotting.layout"] = "fruchterman_reingold" ig.config["plotting.palette"] = "rainbow" From e5fc828621a54216a2c89940dbd5fcebe8fe2320 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 18 Dec 2021 13:34:00 +1100 Subject: [PATCH 0579/1681] Update edge labels to be aligned and have backgrounds --- .../assets/shortest_path_visualisation.py | 8 +++++--- .../shortest_paths/figures/shortest_path.png | Bin 30185 -> 29787 bytes .../shortest_paths/shortest_paths.rst | 14 +++++++------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py b/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py index 485326035..47eff84f6 100644 --- a/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py +++ b/doc/source/tutorials/shortest_paths/assets/shortest_path_visualisation.py @@ -16,7 +16,7 @@ # Plot graph g.es['width'] = 0.5 -g.es[results[0]]['width'] = 2 +g.es[results[0]]['width'] = 2.5 fig, ax = plt.subplots() ig.plot( @@ -26,6 +26,8 @@ vertex_color='steelblue', vertex_label=range(g.vcount()), edge_width=g.es['width'], - edge_label=g.es["weight"] + edge_label=g.es["weight"], + edge_color='#666', + edge_align_label=True, + edge_background='white' ) -fig.savefig('../figures/shortest_path.png', dpi=100) diff --git a/doc/source/tutorials/shortest_paths/figures/shortest_path.png b/doc/source/tutorials/shortest_paths/figures/shortest_path.png index cbfacff6e3fc6a1b30caf954375ee8dc6ef93927..d6cd6a60d4e88dfa7eed414aec3f9c805b5dddee 100644 GIT binary patch literal 29787 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 E0UiHjHvj+t diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index 34a99a707..d045204c2 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -81,9 +81,6 @@ In case you are wondering how the visualization figure was done, here's the code import igraph as ig import matplotlib.pyplot as plt - import igraph as ig - import matplotlib.pyplot as plt - # Construct the graph g = ig.Graph( 6, @@ -91,12 +88,12 @@ In case you are wondering how the visualization figure was done, here's the code ) g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] - # Get the shortest paths along edges + # Get a shortest path along edges results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] - # Plot the graph + # Plot graph g.es['width'] = 0.5 - g.es[results[0]]['width'] = 2 + g.es[results[0]]['width'] = 2.5 fig, ax = plt.subplots() ig.plot( @@ -106,5 +103,8 @@ In case you are wondering how the visualization figure was done, here's the code vertex_color='steelblue', vertex_label=range(g.vcount()), edge_width=g.es['width'], - edge_label=g.es["weight"] + edge_label=g.es["weight"], + edge_color='#666', + edge_align_label=True, + edge_background='white' ) From 5f48a5c2adda405b920f6d1548d617324a585d03 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 18 Dec 2021 13:48:54 +1100 Subject: [PATCH 0580/1681] Update tutorial to use `ig.rescale()` --- .../betweenness/assets/betweenness.py | 27 ++++++---------- .../tutorials/betweenness/betweenness.rst | 29 ++++++++---------- .../betweenness/figures/betweenness.png | Bin 380066 -> 371944 bytes 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/doc/source/tutorials/betweenness/assets/betweenness.py b/doc/source/tutorials/betweenness/assets/betweenness.py index af8c749ad..f8409704c 100644 --- a/doc/source/tutorials/betweenness/assets/betweenness.py +++ b/doc/source/tutorials/betweenness/assets/betweenness.py @@ -8,18 +8,10 @@ g = ig.Graph.Barabasi(n=200, m=2) # Calculate vertex betweenness and scale it to be between 0.0 and 1.0 -vertex_betweenness = g.betweenness() -vertex_betweenness = [math.pow(i, 1/3) for i in vertex_betweenness] # scale values so transition is smoother -min_vertex_betweenness = min(vertex_betweenness) -max_vertex_betweenness = max(vertex_betweenness) -vertex_betweenness = [(i - min_vertex_betweenness) / (max_vertex_betweenness - min_vertex_betweenness) for i in vertex_betweenness] - -# Calculate edge betweenness and scale it to be between 0.0 and 1.0 -edge_betweenness = g.edge_betweenness() -edge_betweenness = [math.pow(i, 1/2) for i in edge_betweenness] # scale values so transition is smoother -min_edge_betweenness = min(edge_betweenness) -max_edge_betweenness = max(edge_betweenness) -edge_betweenness = [(i - min_edge_betweenness) / (max_edge_betweenness - min_edge_betweenness) for i in edge_betweenness] +vertex_betweenness = ig.rescale(g.betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/3)) +edge_betweenness = ig.rescale(g.edge_betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/2)) # Plot the graph fig, ax = plt.subplots() @@ -28,12 +20,13 @@ target=ax, layout="fruchterman_reingold", palette=ig.GradientPalette("white", "midnightblue"), - vertex_color=[int(betweenness * 255) for betweenness in vertex_betweenness], # colors are integers between 0 and 255 - edge_color=[int(betweenness * 255) for betweenness in edge_betweenness], - vertex_size=[betweenness*0.5+0.1 for betweenness in vertex_betweenness], # vertex_size is between 0.1 and 0.6 - edge_width=[betweenness*0.5+0.5 for betweenness in edge_betweenness], # edge_width is between 0.5 and 1 + vertex_color=list(map(int, + ig.rescale(vertex_betweenness, (0, 255), clamp=True))), + edge_color=list(map(int, + ig.rescale(edge_betweenness, (0, 255), clamp=True))), + vertex_size=ig.rescale(vertex_betweenness, (0.1, 0.6)), + edge_width=ig.rescale(edge_betweenness, (0.5, 1.0)), vertex_frame_width=0.2, ) plt.show() -# fig.savefig("../figures/betweenness.png", dpi=200) diff --git a/doc/source/tutorials/betweenness/betweenness.rst b/doc/source/tutorials/betweenness/betweenness.rst index 0edc7fd36..63a8b48fd 100644 --- a/doc/source/tutorials/betweenness/betweenness.rst +++ b/doc/source/tutorials/betweenness/betweenness.rst @@ -25,18 +25,10 @@ This example will demonstrate how to visualize both vertex and edge betweenness g = ig.Graph.Barabasi(n=200, m=2) # Calculate vertex betweenness and scale it to be between 0.0 and 1.0 - vertex_betweenness = g.betweenness() - vertex_betweenness = [math.pow(i, 1/3) for i in vertex_betweenness] - min_vertex_betweenness = min(vertex_betweenness) - max_vertex_betweenness = max(vertex_betweenness) - vertex_betweenness = [(i - min_vertex_betweenness) / (max_vertex_betweenness - min_vertex_betweenness) for i in vertex_betweenness] - - # Calculate edge betweenness and scale it to be between 0.0 and 1.0 - edge_betweenness = g.edge_betweenness() - edge_betweenness = [math.pow(i, 1/2) for i in edge_betweenness] - min_edge_betweenness = min(edge_betweenness) - max_edge_betweenness = max(edge_betweenness) - edge_betweenness = [(i - min_edge_betweenness) / (max_edge_betweenness - min_edge_betweenness) for i in edge_betweenness] + vertex_betweenness = ig.rescale(g.betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/3)) + edge_betweenness = ig.rescale(g.edge_betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/2)) # Plot the graph fig, ax = plt.subplots() @@ -44,15 +36,18 @@ This example will demonstrate how to visualize both vertex and edge betweenness g, target=ax, layout="fruchterman_reingold", - palette=ig.GradientPalette("white", "midnightblue"), # define a new color palette - vertex_color=[int(betweenness * 255) for betweenness in vertex_betweenness], # colors are integers between 0 and 255 - edge_color=[int(betweenness * 255) for betweenness in edge_betweenness], - vertex_size=[betweenness*0.5+0.1 for betweenness in vertex_betweenness], # vertex_size is between 0.1 and 0.6 - edge_width=[betweenness*0.5+0.5 for betweenness in edge_betweenness], # edge_width is between 0.5 and 1 + palette=ig.GradientPalette("white", "midnightblue"), + vertex_color=list(map(int, + ig.rescale(vertex_betweenness, (0, 255), clamp=True))), + edge_color=list(map(int, + ig.rescale(edge_betweenness, (0, 255), clamp=True))), + vertex_size=ig.rescale(vertex_betweenness, (0.1, 0.6)), + edge_width=ig.rescale(edge_betweenness, (0.5, 1.0)), vertex_frame_width=0.2, ) plt.show() + Note that we scale the betweennesses for the vertices and edges by the cube root and square root respectively. The choice of scaling is arbitrary, but is used to give a smoother, more linear transition in the sizes and colors of nodes and edges. The final output graph is here: .. figure:: ./figures/betweenness.png diff --git a/doc/source/tutorials/betweenness/figures/betweenness.png b/doc/source/tutorials/betweenness/figures/betweenness.png index 1d61677bcda8f5b94013038b2cbe2c016dba5779..a10c9fb2ec89bb70f46710e49067cad68e7fb3f9 100644 GIT binary patch literal 371944 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
    $%kJYVUUr}+CqEp>kRXmIFGff{ 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%0DkK!~jVz__6-z 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 |sTa=1G+5S8`tl&oU0%vH)XZ{(9eu!j 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 z zRXqER?0lGD0l{v>5eNyp@lwM@Cf~k=Fy=t=J5_s22k+>m5IR6)w@#}4PXTsLRUppH zY(kpp;`OMElD_r0=8UB~EWu&>mf7}C_Ose#A0_iJX zcEWDDva2mu8;HUr{<>4&F?Pz*-}y!)VH|}Jg3{qPe*&rYf|EipJwY4Pl>$tf5a7ne zZBC|~ONAGNkAv+1ky$vp)B`~y(FS85L322E(vwt5oUy*t75kJF+cEpe2*eR^;XQi( z08yIo=m+j7!=4A_4;0^#3`~{Fwp9r7g95xqJz3)(vsh0%oRUUvDMEZ@`w32v!IE>F zP3UvO-n2ahfwJZE-r}dqt0bYttN{<|ce9fJt(-NqH(eO6SZ-R7c5H4;i=htNV2@lL z&oeSqf!xvA`5#jKD767)fk_vyWcI$g=XnaR!19MH)5xKFdjBXln$M!$aT!fA2` zopG>3n5T>H5+gK$J=hen7{j9s-L%jMkJu@%-}Nzw7e>|#dM-;~#~^e;pb{c#TDfy8 zQa-auIRsf+pjkg5g9q{^f**$VCxP2|~iG@%GK8~i$?L?ZF;f0S^A=GdV_F}E)( zMKmD#a0lWamd)9r8JYgw?Jtoh-UJ7g`?kJqrH);HbP-+ttqp5N((%EO;V^CPum`tq zeOw&%-45xB8GSSRi|gk19;>G#Tfdk*B#tor{k9FWk?N!bR90Q$&PmvW(LaeR{P!eZ z7zQ)<@#8|Ar4+K^x;!eZ5s(2z`_PI=st@Nw0D&F5QCE)eCj*s zwAmrFE>>E6{FR=>ka{LQD^>+WUn}ZG z9Sad<4mXo{=AnIKja&GX{?*CRUv<*91o#KF6V~S(7OLW(oH%b3j1v+nSS*CE882}p zb`qjrFm57|9z}U5Qfsh_xuk%GLbPT?{f#W@nDA2)Pl5?0gt1xAa*xzc4dQV^4+j`W zN=N8*;0l}qi~P_iUn)Dg{$qb$?$_FVVMuf;n@&;*8lF}>;7>s)z&K5#L#2h$0k7QR zD}P(<=g_qh(!)LyZ}u>pf4lgKDs5@m#E(^hHFA1pP5=k|y(enl>Rx1E;QCR*A+7WE z))m7a&tQSVRfof;?xzdN8M&Ykz{~bHS{Fe-`$##nT2z+Z?1z}uT5K?EHjckN()5B2so zI!y181o7(MFHUW%c+b$0(qM5G@zC&N;kQ74v9qQ(Df`98jplU<_r@ zfBMxYnD{0wA%r7@fmY517uagkpOZv8>jg%H!vPjSN6E?{Tg3&EQy(TLOE&j}h%}ZT zoIu3pYwF`6`UnDyKdM)%>e}$+iT;WoB{T9H*ahvk(zrkOnKsdX?G#(U{T_;L1*ltb zN9*te0~E)+Z#2A)Y1OJ`C6RA=HGkWS$oIhJ)>Cqp9fdm>emzLq5r71L{*5d@ z%*gpITS+2kTSi9_9~lVsT;7}WfArOxV@^HthP%XbD0{_A8M3a1T{-d$p4}P^O;x` z>ZT(vneW;ua=Xp8k1)EY*4ND!{E6S70U*qtC(kcGq-Yo&1+3Yny5hqEiOcyM>3cPavrV=GPDlD4j9F}J5kja z%=T3LAcihG*a9#Mj1k&wlZk)eTFQzLt`;jN9oXEd?|kk@nzjc-5itQ{Tl z;upE7r1X?w`}bA&YeCrEt4SDV6>?%%V4bLYh%72lY}}GI1>fLCV%qP3%>e=gj#O>* zc_3a5$-c;n*cDd#Yf9D zEgo?;D{jM>ygqViW6&U7NGF$P zKa2LxW9fT-L2@uD{T@IXzO4D`yXR57hB!9=4hsN;gV7|;MJ&TzVwIL#AMJ?vE>FM0JuUF~=-}&GQ=WjksUA@{^*z{pG35m&vGTv2x3_$~ zcb<;dNy>FQ6VO1q;4v)+o@b;BX-IJFL9y`Z2fHi}$D>?AYMpWhA%F`~QhW|^@oh-_ zMI=fbwkWKeNF4^wQI5;sV`GqyIj~S{(nP2)ow9B`JKNkczRkQAO+sw}52Tm&=;;8^ z!Ir%4rEM$7vT%1Kc?A&(_k!Q~>1p@&M$$=aQsVj$B-#mGGR$mH(=o=j6L5rYtA%C4 z=T9_So|NW@(xk6Ij_~A=VxMFKp+*pU7uKz`3Y&7=5tEf5FmW_ z&7~qj+=rgP5Mr3nB2n=@hz`&LzPYatk87)6oz?$Ug7MmWr~C7PNq*QAViSIz6*~@@ zC2B)rgK0y!lEbrNb%vKvFP~U!vzFRx=+2}G4FD+4k&3Zo?#1H#zbC!#H$OR5LG^bl zoMq#c?x-(=$3p6KBC7%-)G*zG@c*ozsWD}SLwk*_{QBDGP1ViS{L3(5G?=|#>byNq zWX80P_9yt}@FWvdpU~<+$~l4APEH^WTXG-(5s+0VkbJtfF}XfXAgCN6hxpLXx9#D( zDaL~2C1N&!iO5^Sm+B_IyirM)dChFL&*IMAyCPm&;wmaCn0e-3uHA*44<7LykPTkC zSRZZp!2KLY@TVjHI4=-LPh(=aW9U3XYW`$<87zAck_Pe@I1olOqNZW;X3BKppYU_z z-GYSp%YSbtH6Ez-Ie~m;e$dK$(s=XP04hVtf$C7$k+ZqB;G4n5TlbX}?9tTJdyf}y zyy4PHQG-8QQ8)qU6tspx6EI!j#Uu&M1c_Ny6#-&aJW1Y2_;K4wxl87?FA-sc@Neh^ zxyhM{?Lvd7Cu8mzQ?kLHgw^~ptKPGqMvZlkR2p;hKJ&2be}zA15Ek@b?U=doqycAD z1ogKi8R#${qLB^m3;4YQDAEvY%DCT$nH)c4DhKDGUNj+U_j@>YzrH~C#^LGYhG6_E za<6*&v=4E6y}pf^V^ znSQ-vEuJKos|{QlY~T8qYI1z3!igEOhLu^GuIfuikVO&K)1%-dqV8^tCPAngqxX1t zR$U>BFyCiJtR4kJ7a@5G?{4&8572+AN#fEv{e5lKHDBDPJ(X2X$flLw48PgQ(bLQmgh__HlDR*vYu+jGjJ=*vbAhc8=PzthsiI`21?gL{qA@vU13Lpu5yn7*; zn(8>jZO02?!WTEw#}iMcUf>3qtr^bn#eC!Qzc3V>7^4t~_Rs4qgx0uc)O z5Qh@PPEyOjY>qz#dHvVZh>?%Vkqwb6p4;e(Rx2B2 zk8x>Edzd*a<&3v`jvXuTTQ(6Qr~!b2xpzI2|6ARzI|@ zdb3J#L|w@5Czt`zAV)z_MCdqpUQiH=yx;YY`Sjx}$=lRO-|kW~H1urT7(zcIx&WZN zz!QVq@eb$K8Y)Pj{V#4A76DHIgBEb0<~`4s-d+_`)7ioW({txSKuhpZnSCXc`898A z|DOx_Fvlj7nBdeEI!+{b!Y7(Ng<7v0GCHc|B`L;0*`}-0} zG&ObQ*?WcQr1tJR0V72FU>;q8i10TgfA5El8(;gi?$i8Zaw()RzvLK?$arlFU}Kq| zj~iHAH{cA=v?C*KgfU84*=;H}Xdox~BJmV4jgxCN9VIcG1SIeyP_p;7D$Un7ruioS zopYS8-l+e%c6r?8A^LD!MC$DwQ_*s8b6>lo&vg;8oXm-x*axj_ zd1UizUf)rshL|GM+z3Eh~y9gtqSM?;K{HTJHR^$-<{r0s$Ybk=#EeV$RLT zd1>R1EkJF@jUPvh92c~8gdCP9Y)B-8&V0p{%Dn$^i+tbguU~;}`sWtj*l~RQ6)GlguYuMa$>LGyr=7=>GPj7>;Hu z0Y$pd9VoU0G;>u3~rmt!UCeB zvln=tAj(;_z-yb7@P9Lr{hJycAaZrfB;ca2)I}Ygqq}z>PUwV}Ef7qOh^bWGx@Nk@ z@~Ch2M3etRBi2OK6m8?bAq0WO*58ZHeHWQ`r%ST;u6s?F$WqGAxk{#`*-;YjfdEzL z_snIl=G)aPY{bN@t5=2RkOL&R{CB}CK8us@o>!EcxU8vZOJ%U;W82zTWvWC~zX2Xj zo3pWV31z!uxLJ*BsDg~>@BaMLD70tVjxr!so|$QKK04~D*)BC}9f|zreKU98Ji7qR zj*u*s^|?~rywDx(Oqa}1grKNIvfpp4Vf=l;c3aIRf6exW7hKFT?{kf7V%0dBEhRUo zF{Fp(%~)K&nXCB>pcFxT+3h%}Q;Xck66FV8Wn?`1^CzNIOX2n@FxCeTm17n7&_~rm z(OtS5`s@^@lUbS%@l0!JJ!wE_Cb)Y!RwY8idzirm(kB@V8*n*2Rd)<;wb>1kwgmkd{EHF30m z{3!d$RwF~hxhP6f+rSnbwAe+O%?crg0&xpg?cKZgBC%=_r!H%-8$AUYCAYZ27L+Xz z9*VYwq@qw$Q(nZ5&P{wdojrBhVXR>XZAYD8ZML_OF(}2h7n+vX!S4%SDn^`nY~1qF zMC2s|vGQEiW?4wy$6z4KN^a@U?alc1&#DR~rF?6efzny|>7?3cOjJrOT!zpZ>BXWs zt)=s-zCI(xF?8=CQkt6BPlgKT&alS{9XfFx6-crgO_*s8UK8v(Ng*e42x7~>JTbL zvq1;GCzea^97HeTH!ONK#@$~zw+cU>cU~UXg*U(O5&|Al;!CWxSSh%9^`vz%MEQs` z$}B1hM4k1}e7(gAezeccDNq5fZfzT5|D=WziX(`tmQzc0l>5p77GLknmJQFUD z!aON~I96JfPL@_FTGJ4@L#Z5O3HrI#C0d3}44btJD{$*>5bb)uRixV))(?ihy`9KG zn9wGtW-Pgu5u^DtGO0uOd`(T1;*Br7yn-EaCs-#OIV=-T9CuFZ%`%TJ9LTBRQ4ka5 z3egcwj6*AB#}wu?nbRERZE?z?dQ7@%&tUd<`qsTa^=U=yc>VZ+t}8LI9q&U5=)%i% zIVgAeFp>?3Cek8(137~eEeIuhX|~Mg@3ZSHw|0)@_SRq9;mY-w65RfHpW+3Jofbz= z~8jt-Ay>c@WSE(;-b}N zw6nui;`KSW&l>lI#5;+zBzkcfZT_g`I52RI_|& zzrw1JihX{ft#+&8)rykvRA!cwmle(awkz)lkweDT)Xy8o&z`My-*X60H=w7uxK^C_ z!gLTl2avXtiIlI5j2}QO)9>0NzwjW|CEj+uqH}vDr__dNhZOA*WY0g1i%^vQ^Jk8b zo+4mvzPh@ueBs8x+R$x=k>Br*12dSxb((dKQq)6q{S&wf_MtNv_ z@P2NQm(7hZ_qOTBD1x_J!?UB#b(E5d)+~Fmaq@gx8t(<>@4#AIE5g#1vN4n4I|lQV z$`LZj6&;b3bl}VUX{?$)yH0p|x`Hd1ny-lIs94(7{Jk!lsW(coF(E;$SU;eja0K=H zcldwL`fvmyi4=)mNWU>l{r2O&S=&mg z+J2qa!$ya3ll^n-5h*>sKX zmZkW>k;9^|bbo?YMz<#pX>?DBCa79Y%C$w1BXF9Y=Sf@k$?WVP)b18nu3Jdf*Yk>K zZaumBd0LMr-SbNAK#^-k=5F-ciIfNcU-a}mV`^&m&_B_|MZRI&kLcQI8n7+W0%hXh=g!2+fDqd9{DRzda|DP@1-rLJFu&HJz1UYhrXk}q2aWtDT~MN z`WpWpD~mg#7X+~6HjXPWhZ7w=O7&;s1gOwfk&T*H-EZOhgR}TFXm=eW5ntp@W@$cC z+wZX)d1Ph1YrFC2fw#N&8I58$oOKTi3%hGUqSB7T{Kt=#AJZ}Q`y|VfdNbMDzD8cA zpEP9{Y%mS`!Aknbq=3e4GTLaq>UOz!UsOyyAvqenPO_HfuOgQl=NcQ?;mvHE4r1QF z3*Tu+i6+$QDJF?Zz(`7xS< z@66crJ88Eglj)wndGlz{-dIs@U~_3n|GytrF+TG-rtMd=ZhH2<+nq`P*5Q3&8qx?GSa{!d5 zNd0QB8I*Jx1t%2i=IbC&n}#;Rt{Ynk>OqEJzd1@!oee;cIC>?5?x=Xat&CAK3KDz zI%S`0D-*}4sK^+0QduwdSdeTN?lm?lCB!Gn!E!QalYHo-kh-q0Bsb~vgbW0_<8a*X z$4WBO{WucC?$Wp^=GcRK57k~%X5Yd;BcQgn68B|q{@3mIL`_Giez?jvIHY`ipvwmY zut?Yo%`8g9Ms)XETAI}P%B3|}pU;o7y}@bf^uF!ei=c-O$;;0l>zpo~WKS%AGF-Y# zN;v;L2#B;4CZ*Ff@uzNxw9V!l*C?c`W=2;8ssq&`FMxTiLX%OproYyhP2UM$d!rL zf8Qv!Mt~R|JFwD%stHfao=tvw`{q{2ipc>$6hL)fzGQy*kPb5;vVT;>*S;uVjOr5a zC0wM@6raBy;7YYc=yj7Bg%8T%fujy(HYS@j8>8d25jDM;@53?Xk+Ot_*s zDbeuzPsaMzkXoM==Lr!iF4iU{M(hK^8A*G4cfzLcv%_mu$E!AZYlb1^!Q`MxZmX9^LE<$%* zRV7Fy^)%)@j_bVNf1SUo1iX7k4pFzq3KQug&|^>WAQLN?A5Twbf0+%Oi3ouZwi}3( zDy-7ayy86fN%(ETdsMp~AE>%}zux~+R(4S*r#qeskXgIDKWP~<5c~PK+{rU9E-#M_ z2sk57FVxsL9bFPG|A6JvuO&;bUo*0N+fMM;@E`c*K!mMBo?UDt>vzLRiFb%ou33iE z5~H;E z$;rce_K4EbDiX%AXz8iMT@^!N{oHzgzt0AZP%ZPtAH`@ezAD!%= z)E-vRM16KYUY#fhCa3RFt%@Z1Rek=w$HvO1RSRwP4$Wc(Qn$qP@c+pA?s%;C{(qHG zQpqeDl0D1TpzKw$M@E@thL9anA!KKkmF#S?lI)O`gk&d`kdXCzUY+~<*YBTm?#KN& z$H{elKJWMI^_+3UM~=B*x;CE*#fp@bbz3`6Oxo3z7mA6ZqMeDIlyNFCGE(ukn@n0s z2BsV-WwYG0+s1pZ_Hz5@lfj}SEH2m{a6*XlwpSlfEnECnQj8LwVF#((6qXYp~IJcfpQ$OB$acjOUah$NU|#B2iY&&W{mdL-C@xD&wsvHW+T4625L=3j=7lm;K|I(m07u(*G2Z?e1V!S zJtgJPgOd-nxx*90Bfdzbu1LYp@#p8!s8&f#KH52NFja}nL1#E5?&g??g6D!;y_a0y ze_$Itl$ptXXCh?g)FBGkq&AnTr~Y~#6k=dSZb^WjwcSAVL}&&=<=_6keHYX*6I9Ui zW75aY>+{B(Lxv<5pOuslC0*vtDF%el9@EeBL=k-#h#1C;j*rgF zg6#rx`1G#hy6ws5{W1DB6`D>Uk*8wi^HeaGFc-)B-tLr42n1!A1F)6V`H(*4N47TT z$0F+i@xj`wIyzEpk?>;FPA&ABgfEhlkseXG?0)km=fvAUht9-8DCO(X+73k3s9+2Q zUVI=i=D#fT!AC$9V8Z9i8^pZ7`4lzv`dX9dRzOoz6SucFU+}5OR;(#1wm;s8-a}_c zj#ih*^)4iN&%Qe%b?QhcH90GGFf7+yeNp!HU9q{1OM4YrtP3qZJM-^WZw;@1mI8PM zMyW1#+Q7-SN19wD2n|CSPC&y!GYdI}27nkM3CD>FL?7wrnPBG<|MqQqS^#O3^|KUc z$!PslIQAi`c3N3eT8%l1$pLK`I;?aG(XM0i$1qxXr5ewi&nB)ED) z|3|5?$qe>Qw++<*rl8c-IitY;URJ@PK^~lFO-;j#>2)t!E=WsrHa0f7%0BrcbxVw; zNX4p-*W+qbFm7q!8)LV27VF7tXv{iq8sJsNcZC5g!T!(5AZEaJ9fdAzkvGpB%ep8w z@ZLt?CMPfNlT30IwqBh3hmJZ&wkjOD;rP*;i7DE9byKa9h(-RIAvss{PRI(hxqpRiuO86lxti~O zoaf$!FZ;H-w%05HeIHYWh7VY3am)_`#RrG;v>Qch3nP({@j#-)NLx+G>fl)*W#;Wx zSGZ_VjNxG9RT>NL{i`Z#H4b9EWgrY@d%sF$U&s_}!3atGW$9_1Pv=^KLOR|sZxDm# z=F@sq>h)i~P$21UClShllTdWXPf4X54fx6eqMX9NW9IdJl%oeCRoM~C^6e(A+F|Be2F z%kQRd6UQ*uz=%O2BotMhx35xk=P}hoc;)a+RIdGvxD_kK9By)>1?7&<0b&^@ zEx%H=OV`$JCC$BpFpA}B15sqOnVX-$wuCwkJJ;QH#zmo=(d5{BpLCqTNRN%MvZE4~ zVT^4!;gJ)8fCq$-;A?DXxa?WGduod0p492jcP5TN;jF1e%jeD*twe(Lrj-ObYeMEy zTT@8KqS-+fEfa7t{{H*_t`jQ6KCt4OJ zCW?uTA6+}8#6%g{`d;}WB{2j|biwIoC<4X_5cg*z-$v#^hs`Yx!oS#I2pF4Ae&LI;kjW|M9Wb(1$H-R(n;*VmnRVxj`2A4feA9Ubn>H^h_SU%jz-n2r7XM;mYa zaZ>NpzLlb3opZsnao&S;a(vHST;1e*L+(kb=Nn_w3Jq0?XcdCJtereNm3bG&Is6r4 z1|~MKJGY+b@&p1K6QWr%-gb_UiRl2jxwy4baQ)6ld@F~Q%y(CfbX7VD@)&X*d{lq! z5oiatlPFsMEU>4qU#k(5Eh0nOoVt<-I~FG2rYUj)A%N`$*dKC(X^^gP;X+YAzo9<# z*(A8ITNU}wpQomv5Ce9PXX|123)Tc$bCZIX?=NU+9o#Sfcx&q>{LdFN)FEfOeDmfG zRLA;bK%%b`Yr^+v^YOiBe~_)B;&5kE2rps@-udp5#zX_C9Wu?^XWAL<6QwiM)%9+s zJa1Cc(i+psNiZ#B0j~lWY~fBsLsCf}VhUF~rx4`K!*(^=uXt*^}^ya`n%o^SHteXROdaV6(Br%*h%Wb|0IVJi* zs-+?-MJ8BI)s>>dwDsOKo*0}!Z9$4Sq+j0!tc{;t@(j_Lm2V(P1rOe0Cd0tM!aCC! zcL$p~pmpSZ+}~V(aKonWS?}zrgbeEmtAx8wrdQY^8)u{PF9Oj;Y`Rrx#E|`{l$+bG z#>U|7pA~eIMf<~!9c{2%DfU<)!rN=$w10lccX<9($aGS|)qgAK!kpayNS6vFgw?XK zRbwns%*?RK%+8kjHm7_l?(?8)3q64%d3#T%^RvfUsn;jNiKV44!#G8tS~Q>7w@iYG zM3uRt_3SI>OR=7Lecm~ER@<{LJUH0`*#3s5RfQG@lJ_zmNkStXC#ddWlfN4~KDrsG ztv?8X%0$l!LIZGJ=TvmR$jHce1Qtei@W2i(?!D-}E(O2#8b7GUgA7YC$m>iM7h2lG z2ns=+B#g{LP~I&50X>Y#qzOMMnDwA+I1pz^|2~78Ju7bsdrPM?tR#a0X4;`dLJXzp>OJYmFVHaNA3Ww& z`$DCKWM`qci$l%UMp0UKB{9>fp^w>2(Mr^T3e;P}mH7kKt9_Yew^n~0VhD-AhmRYR zV*i=RCau=mVZaKH!m9QVr&Cnu7%#V(p~=N$u;@(Xi2KsCHfTW!V;Kp#?i+sKav$2b zZohx)u6r#~AgXJ5bG z?EF(U#G?t_uBFJtAcavQ88EQJw>}!2E2hP3Pje|AlV*{hMm&qmt}R9^34shsHV%c4?xtW);PmK&udQG&u#h}=blvb3q`9#Vh0 zi-eF=?1t@@L~;eHzVrm5wB{w}T1`Pkb7(ueULB)>>zlCz_^N1!x@_O0K7- zucg&e>C~&Dx*pibMSqmtffoZ8&NBtoE@E7YF7w9_`X1w!z2r)+&N<8AAv(qLZ>+5z z`9FX*7&;s?&td5~c3O2h<=b|AJoZOI{@6r^Fdny6BSAGAs3tQ$k~VOUN`Wl#uX z_jM97TEDUDWOx~n%PkQ1J(X>5Do5VT@7ov3{=(X?=ck}<6%|8=Ze)CUOMep7vtFB= z{<0*iSD3oL{Hi)BssHKA$%&Jbt%_V+Jpbacu6WA!Xys@Xn#$8VJ45n+_?pR^c52ll zCKF9`m4{x>hlQ&sP4)u`1$Qdm{5+xk)ho)%R6l&bAV^g1PPf!H(p?7(cP1&NpsN!D zBFf*@HO-#o=O4NYeC*4Y`mU2f!n@y;9()(45^?S2asROz&}|)nsgR!?!(SjvbKSA0 z;q5)LSnhl6v{EUL7bo~w^Z`;Ly84aOF8v>CrYRW+^YJ`9_FbZirw+yLc{6Tua;VJt*L1?>zhW}lsOUIQM9Am|iCN_7#tYFw zoA?bz(Xw|UFM=EoL%5n+Rdqj2z8PkkAY8k6pCpX$>GE(MZ!Zr5@o_++A_&$e6+9ET z^PnzhhG2E+leHzHF#%EV#Jt5_f8~_1ubGh5;H%Q4V#^=67zx8LsJYaKo(6f^)VNA1 z4k)uSi|?dl>G@eU6a!Vx+nj=-DS z4E1LzDNz7weJ{{t3O?*TJc*r!@RgBHf3H^0it77`xgUR&MKP#OzI2GLoBIh4P#S2K z{5q7~n z_w=P~&9*@Y>sqPP3DJ&v4P#vK5MZ{vRrX0_xU}?3SR04wOGXRE7`^kx!j$-sp!kkx!pOx zGQ2uGN)R9>lRf<(h1vRlub$2Y zu_KnaQ@}~7MLmQ+n!B|0gX`_HpZgK3i&u^KS!Lz#`&@e+aRaDa{%f(PxkxtWOFjwc z9pHZ!3CH)M7~5752sVOI3AvN8(#b{g^>07 zIldj!rm)+}$*~kEs<$gpuOP6M21Vz*2P8MtkTM)gLqB@E|0ytOyA`_+1wM~`cd*xA zgR>Xy1T0m~8XAX_U~((f`r;UH2!z=Aqt7aIndN}gJgcqMEfbE7rGKaA9a>!?|zX2Zn@=Q@a#=VBl|l>7iA&%F*mnJc6&~S@Zv6SmwSL_ z5lyfauNW}w1}}E#U@lD1ctQ~1qRjQSKZo(5T;xZa+QGRqO7;c&Kn1u}l3b*meB%4< z8?%*F&}0Ig_}S^ziiD$QUztVg+vY`M8RoLe(+*)$5EYV2Sv|tN5zrE{r~3w0aRm>L z#(ip9ii4>UR@NtmEH0}-TW8_eqbw93#pqE$ryiOab44LY@DxKSu>BK}xq&kA14eV0 zUzvg;#sv~+)v{=S!ByUt;}TO#>3#u$)bq?ughxw`2ODoJCU$9XMPp*{irHQb+y0{Q z^p$n@he3lqk@|V*H8m#nqi8j2#Owmx7msJ{S4P1B@{5mcRxpNRslriN>|)y>962Gn zb1+xoOj4`m_7nlvG1#%~WRd^xg&a7HCk7XtUAfSXfmxQl*PNZZx5a{Saa&;hU_05< zr|I6%Q0vrj|FY%o5V^MTSK(Z3H-uU{lPnL)>K3Mke)!<%?(w5y0naoJG_>?OKono(!5br>A3vNYlgV;xVX5pnlPbpVc0e?MKoyiuc0sMrm z8N@tX;%!7?TiL0CGxCFoR5An*6WLhnHFIaFgZ|eV+v!8U)73eZl(+_~*X)ECb^+!I zkh&d)mV!?f-$a#(zlv$AP4l+T(9ni61`$N2Jv=e3(H6ST*$j=x@7ylj*D#v6RdaC8szr{xGdo0MhtCKUU%Fh z1R_KhU!g zLw_JTb=w{XTFtI~*b?)dFDmh4rza53A_-w0 zH*Yc3=5AM0s(g&zR^&LIM+~G);W~gLfH%(GmixR?$;cM*szF8oUpd^# z(lMy7N9c5%O4rTlT1pT*wGvr0*rpygHAOJ$3Y`eI5$FCPg;x*1+$oXiCTJRhS+UAg zBH#u`zeLTgD#s5dHAd2Bj7Z6=4wyy%X-%d4GJ@4R96I%w+nL~_&f_ffa1Ve zd|4Qn+FAFDlH)*?EbLx3N*~9~A<_H6Qn?U;R?vD9ew<;aj=+ilbs>bCKv~Z#S17TvS4IGY59xk!f=aQ| zLZAGtNf!5IU3e~m30;1g{AQLSLV;lD-yTP7S4z&D>s0IV$B#^stMvnSx;(tea@r_xgEie>Fx{B;{VbnT=ipW3;V#vMehoR5c{|D&_I$=1|-9+=1}0p{>s1+|}= z7pF?$D>yZi+roVN(`}vzU;}S`(vu=!mOP zpm5>yO8zv#GlPEh;mt9;+2_Y^cIH@-PR?JoM#ZMY0wOaeimzf|CqXbooHpGd^|fZY^Lk@%||&|g^O9Lw&pil z>f8CB*4wQ(ZLErAUcQ9DJ3(%p50OfWR<}SX^TJe zDL5C>0R?iAl4VFm0XPOd$rXM3=Wail#|p$>HVW~&^ME_5UoQHFIH*1>U!nRo2*VjE!LLR}?qBLp_JajX8CRoy-eO4jM~(EImoprJ<> zpbTI|=4Bldx)UBu-BDfK6NfJ3;`EgujE_O!GC z<$W!9#~w5^|N6zkC|bK+G}C)Ybfmt#c9|6c8unLmz%z5q44Dh)0eys6NJ%jerCEwK zGq*`$w`K^C0`dG9h^Yv-a4zy_koS}+d7gz@1wse?3+fRgj>=~-)q)b@ z&u;>jd8(zYPl!H2Dsh;k_d_hjkY4Kjc74-Ge9dkUIiyhHIQ{)iQ&8)EAHNKSv)g@$ zE`gv7Ly%mS=3|xTctas9!SJ})5dfPmU{1>Y&GSXc-$@aJt+QTj3IJwuulHMJ2y8q5z!r_uji z_-j3bkskwQcJ>?bu8>C+lcrOS0aJ`jbnR1$asg5(D405e-$5(qLzmmnR(78^JsAnR zN$It(P6Gfrbl3qP5N_`#izUGP{OiXe70(V?9S7@s%HG3{#mXv|;N(BbkWxNy84;kZ z5!uFb^XbE4NqB?*yF52I;t15F9BeQ+m<}^KQHLqtlY=}2QV8+{OU3beGeenfb)TX{2? zE^#O;cc`eU5?-`a4!wg3@qDTqtW>ikw4V4nb$MFBNjn)8Xmj7h)1Uk9QTGe;kymWY z;bPr49LtVOR{M3Cbf@)pJf)n83Iy4K;StIW%t#RY-LU0-T3n0I4_`9m7mL#gyYOBU z`jl?9^ZtPDrw6*9+kLWVd7YY~gF^yZ4jDBwPtUtoT7nxKz|5&DDKqYVkTFiCmRPLx zu=oJ%tm6x(&LmkJTZ{E?)dVZMm%px>KCSRbw9-XW(<~eAs;QW(cgYYQj9cR2fYHz; zRaHrVBtL&zAO?VFWwdTr&a$TklxK)Fi2*iJlPGT5fI22G5B4gTB-TTZmQe25`R7Mc z^}JV(`0X3SAeWRfCZ}S%-f>ybI4V1snsQ<)TyPN8(2&T^$zZ;Z{|PmRRLaM5JHd`S zt^VS`Th@6(1Nx3r48kzLp#RAHtXIBxNx}Rn<(S;7w0Z>AJSK>MGR@g#(RtJ1GU2mTW93HIQ zbU_1x&&=CE>Rf|L$NJ^m7cYN>huKVK#C8I7AMe*$e>u4E{bP3}ht`cwsoM9}wB(#u zJ50KPgYLLh>4y(oI*SI)_I%oTRu>4VU{|E1rT4_CXtRxBCdRIbrV^ZwW7ji1e}3qv zP3zBwzYlo^m#nQTbBTy2owR9W<>W`8h=FX)&`?WMD3~^VJ{j|wQbtAU-+2=n+QZ$m zeUCz4S(Zy)w9Ru?6OC~EGy3XPC7nxwsFZ5gJx?c&%vZ#8JV}*@(B69dSF_JX=I@o2 zLa+6tw-XKkWuHcLWJ7nAz@J0(dp!;FDX>jEuPsBM-I56cKCZ^G-9T_5z(ItRm4$#U`>N1wTYJjQ$&b3&keoTX7>#h*E(phrkMm?OMJM5SIJ*BN!#t z+}JNLdz^#Z1yo~n^Z@^RpA%?ozJK2U)IWw@4*t4YvkBL__47t<<_91-lK7ApsejP4 zkfZC|62-(s+Qx*W_svi#=3}a-g(l5R-Uudmv0oyGq+gALruguUEw=zhglDk@@smX= zie_HaDbT^pBko!0melOW z6JUK)f!7d$igy*5eelT78GPelsv3i6OL6ENVsN~oV`+Z7uL(Dnn0)Aw2D5CrOzzZz zE?F+;`xmYeTo*CCu4-!CFnyt#8Yx8_{GyQ^N-ST#Tsz(#TTuC3V({hly^`yLOL+OL zfsBW)&Cy?!#?ReRrygwoai>mqZYDg1G@;OW1jt&N7o zz|TgEnEp{vh=UnTSe$wpY3iK1wbe10aUm-*1Js3({fBZhLp-~7NRp5Q#E$69gjEH8 z{;c*qvl##0Z+Bt19NraM#CNz`SfodUf_1^5g%t!-t!xVALRt;p-Dk z`vlfv8%~CKMnFrz7|TMm0|nIpkJ0)qPk?iQr{L&^1kqbIV_1s(!nJyr(fD{NSkm$M zDzJ3$`?!HYA+6l}lTq|W34oxUo&q@a934?q1G3|q1&GyBws9myQ}1TI%?DV@zs^5R zHasxOG5%_9f`+$*FFYc0Ne8He~$F#E7E71kWE$g668)Q)dG@>QJVlTogdj^4BYHz$l&YO}ZWd>1ZSAD!^YeW4Lh~!~h1k3L_3XbI zdK8EXBvi4y@6%LL>)tq$QSPT>734f=tdV)w0s6ov)=uZuTv}O7ICl>Vz#3TIIE|13vW!ynmTmS&8(F zZ2Gzl>1h=EnF+w=GA~1O5(n9Fh=-LR+f1Ovq=Eb)6qM{5_OKr?U(pju7{kR{>}Y-- zt=5qL3>K!%MYJUi%kU*c7q#7&)_YIzw1x`ZbUMmt34B!VY}-WJ{*!i9^|AG!(T z^AyL7azKjO`TpCt2!>k|3sH&Xe=5618ak7_AcCEWnT7?comMW32BmoLtG5zt!Cm*O(-vd z>*65fcp{yZv#FX&0ZhnB>Ka>?!-yM+N&u?L-0l~vU*$y?LhQf4ouyFINZYAd{k!Mz z&nkNoR@RyF8L6F=Oh@*EmZ@9TE*PK=GhU0QC-z;QOL&pZH3^v}=JmCOumeabv5y1*6AhDdF6G9M7N_t*xfk0%-C3C+ARP)jCv3!!WtS$~te@L4GCi4cSFX z93_s}o}dgFcGh{2O962n1~H=1?=IQsNqeHwvtocyn|Fs9fnQ<((b37s<){gNtT08v z5VFYIC^^~p3Ao^qP$t4AHb8G2@iJJ33Q!mg`Nhtc?<|S|y`M{P%Z}{-)#KC?aw78P zLV#|2EWhR2;?&)~`&*0cJbnV5N6Lvzm8~fWcKKw()}0hR3-(rAQ*Q(J z$jJ@cpWU(hTH&FOAg3VYwruyiuUA()YbzehC-eW^r90t{!|dhaL8L|lXc>Z_7Llw` z7VsDjRSm^cY>lFiHmGF}g0t(GMrFeL9(5P#PXTr7Bgb(+Y|lk>m6y9N)y&s4!QRAX zQJfyyu2=0@`!Ls}ijphZaeuvQ;iZ*vML7-{_Ct?Amb`qH!OL(75?M1U9HfZ9heJSx zg9hg0t}<`7CyBpJO5@G6p0!VnyZWpM4&UxIM7zcIUif-g#v!2zCf=z^IJDMz3TLB= zd$F^BosE0;qUCODIxFgYw7FrsM4JpGRTTS812G5qV>p1g}8#P`Do+ttlTG)Ndv zta#5mnHA%OJtQr?AGT}wa-7EBCcsHB#2O1&!|0gTZ?(|lu{=GP?v4I%b?p%j-P;bmqWNz(av&ce+eQiBBnWiR$SW^&Wxn#$5IjJuM-Ye;>T=448 z*7BTS9G&zf`z&cWTD>~H^!=m5Tx!|HPwd*z+g}0|rpU(bg}-q^7Y0PU_DiV`XyH~f z%ROh{MeLk@7SnmwM};l0Vzjz4K*#vU6i9A1_8QNbv-S{c)&D5xk6w`vJ-07l+|kqy zmr<)(JU9Xr=0Jpx;~&*>&|0UZ`N5bzbTk$CRax00V{&7BL*v`;SACgw|7R0J3Khoc zi01y_f@p&^`JZeQuUjKEDn`Zzzmm`rmM||JccX#nQZv9(KSv8O2z$F0*YDA=-+>Dbfer{RSbU6cpI_>*=RO?U zPPB1p{d_F;&q;^Umaqs=D9zh9Fp=S&5KB^z&ITJ@Tl?hOw^YVY=hiRdBsQ^NyL&kk znAY$6m;PdpGkFs{bQH2T)Ch<$O8I!jB&j@Kz%)(q=$ZD_{EDOKoh2hX@YAe0lx$(` z4Sq7hHBgxV7Yt;9;E>UO)fVlH?$aD*IVbI|0L}D48yAtVOOp0+&=-m*2OQs)}JeAXTL25HtCRIcVIb z{=rV*hQEFhopt?b(i%ZVSCbSFP-<3un9!@9@#2OcssDXUZeR!@JROGa*e^$f3NNaxWs^<4t6TV#kAu`zZ5^M z(fFJX*9k!hgt*}y;#~rC$_pDynRmYU(jKBv_s`e&GBSUzo#VE>KH3gtHvQ_yW&q@+ zPiB(WX$xxpup#YChrf!CUYhHQUc=UA!;v#W*x85$1-)@+Hl8EV4<&lksE)a%#bxw9 zX;#IwO`bnL z0Rc2VCAgL|1;rR0DB(bO2tHz;mR&}#`r#1moLBjx$w-*CwY9?2jvECus-v&ce){t5 z(2z=KG*V#{dYKY8Qne15GdcNh8hS=(xO9?K@0|XSXKiBj?GRJrzNjb~%Khw<)+wc> zo}bquirx5Qh~OaukUKk9OmKcyRZa8eEtN_Y47|jj926!7kB@MT!>G&0XzOr3T!Uzf zZ7ctu7Pf+6a?fkR4(W45MlH$~89_2)P}edr1ZCy&Y?My+R#O9WB3PgPi3L5{BN^}3 z)=FSgx2FziYBdumDHUF7RrRK-lf3jVj|C$f2X|MIUf{zvi{j$#n5|CyEo^2hi8byW z0$5wj(zlh9sHx&263*@4e;GbU@5W^w-aKHYHHei)Y6I-i7c+YS?sr>hVf6|Mf~0y6 zB;99{NHZK4sQVOT)ZGnr1tO&Lc~_LEQ}$~4LM9LYU--#p4(^<%h;w`BS!d8wV1GlqUm(BY zXVrWfEKTLQuWlbr5VeOg0V0F*=g9`^w#`4Ue^#a5pNSbND)?xAiR1-!7lm}=$G27& ziJ27g3V=yjKK%jJDXZu$RHT4aBttTED8a~5w{%w2wD6E_S#At0Zdi05G@KaWLH23U z_{g-SWIWBOKh)gcSs-bf`0X~ z<;?K+N*fUfMbF8zMWR-`D8t&vBO(pc|Na0j*WJ9zcn95T;(N>d>pQP_Z8gg}lyOK#G$1&-`(MJ_s5o zNJJogXvAf{H(A-v^&uh-%=$-*%!U6YGFJ;GEE5)Ng5{Y2szah!WM>$;u>twPu$!eQ zZpwJPLdc>5wggeFkn89O%7T(`Np5eg>q4rAeDwpr+-pk8@~88wyn~?tM5E)Y@iT_I z1ElKV?1A!xAl%1=&~m`1;we0FA~T1kXzX#kl<4;#shT%I?Hi0#g#noSp^j%r=s?*J zHO?XemT)(ntLlkO?JYD4^57i;{*Dk~o3g3eCo;^u&14F6W_w{BhVbvIb7Y{Tb+WX@Zq52$H)2OItgB2?F23CBRl^H=mOR~q0QhIT(zr;uiwKo+}zCL4W z+JGqIx_0}`4^OWc89}xX(=V;FSB-|=pkUY(vJ*r&J2jfGc^)GtPq%^WweVW!Q+3WO zSFgVN_~Zfhb+w#m0r3~Tiayq**?K**$vs*TPbJ!*(p6AXMX{KDnmnr;kzdaxw2neAModU~w6t8U&LeEaU*X@p6Z z16>F7fRK;NZj}LW`F4hR|7IE8{8^XUd$ZM|ci4M5xK>&f)kz&&9G?|Q)UL$feNuC5`d^W&?FJVAVr2#$jD ztg8e}J7?K6b{q?%4zrAxKkfzj@)~mYT`)M|X(2w*-koL&_b4EtRnLQQG>@7!plu24 zmaGjBW@saWd4c)YLe#SzRCNpNSL^ zj$ZtlmOx-zEG+ys7Gs-{n-~rt3wUbFU^F}}p`mT%GTG=KSeR9{CoF&k)sDzBdk4(F z$WQFhqw9t5M(-Q^v_=$LrUtCi0Hh%+L{?PewfCTqEKV|}Tkm2iwQzPmC@f5{g4)lX z6_S53x4d+B zlaNVkRgTCG1{)v_^a+tgl}_pt&dSPz=3T-W>J?mzYFN8;blw5}gDZuXSErWKCGDBI zVo)1Iasp|By8(gu0h*38+g>S-KYOxY4c8SrF_5Sx71d0kr9kTY zPAzCISmCH-Yg@Z8vD@fE14Rt-?BiBojQaX#*}WJ&YiIx7FW3V?US@~`Z0-M+h-2? za*G(D{l#<7q|(%YU|N))ZsaaFcS);%iqV6gEI1?R^XE-G*s$prdb%A|$}kaGI*rl4 z{1;L{P7f}ghupJ~UUkEvBvzR0;mQnF(=eS%dGKLjb%G2kE^%tR zwMG4(bv`6r5+3Nb?S#_{eL89eHu!JjW5u%ZzFSQZ3s7&bH`rht`0-GQ0=Zveo3GUF zfvgHh?Zku~&R0la#X(PWt3evf3LXxgeAK8zyi^}VTV8y<;k;N9s(jQCtViZLQmV!_ z62q7GyyD~ALQ~Upbs}b5WmLh6vaAuG#NR>MQZ(NC?>$vL=kP>EirEWv^N(sTmfbty zW+L`?Kw3S%ALwZz?F+@Z42(mM)4r&2Y2pKt)pnhKC6V5BF8P;DXj<^!zv{TOwR0ANlsFKL z1nLNS5rTf`^SM!+0T5JUOg?X4JE)2Q7pzeaL4rLRwy4 z)cQm2%y6XyL{(vnm0ey3>m+%%bqSaO6c4XPY&*>}4RgJ(DnyrwSb^J=<5Eq6F6Xw-d-Wk65sOt6&Fdv^5o?G6ZVZ)?wJP1et=PG{vr zUj4T|7T9O(Dl??nLdO?=%la5A9DA zK~*rDjo@AnHICjF@ec#*h*Zr1!I3w9-m0`0>G_C~rkx5}+ zYJZeC>*PBX%A9@~sBP|iRAiz|OMTtD;_xI4NQn2ooSpTAXAd0^#s2 z!gL&>YMf>0w}80lpOpASc6PdR>!oa`j;u=Si7)lnJta6*`I0(e4Gx4u<<|Q>3Z@J3 z!a4o8x^qAfsO|BOy8;qYuuQ>K?)eM1ueyZZA*sFZ&$1>DU5520nvz!9wbx{%J zKLc`|Nz*q%ZV2JFhRgca`}$fhe}Lq-8*S_8CK>Pk6By8iZ72`~2G!iZLhR2q?K*Jo z-QtwGk+z1$-W=_`DQ@F60XW>ZQvq~pX`R5#i;yKE1ByMaoa@%1@7uFP^Xd6m`W@+d zg9Alox9PV$u2b2K4J39oD`)gjP$>MVhd8lZD6sO-u3i3k)D2@jYfDT00|Rd&B5bDQ z>vW$7UM>~35n6mv=iE+bQ`HhH9r z85;J}(00_70i?hz)RAx%@6dJ6jcTer0!ZEa@eb0dGTXUo8;^``j#8Ur=fwOP4ze6m zJsHF)qgM21`k5vdax3oCTu=5~V5QYY_WMzjgX3MH4~1_{n|wPwU)j zAe+bZD=PoGUrbX|)6;AD`a+Wm^f`ow?SEXMR0RtPGbsRZA_RFN;0R$m|F{WcUJ8QY zqza^2US`Dz13J2`ofn1-{>V0w1bc0VS=5NLULAA#_Bxoiv*%Kj5Lfj0{hV#OPoi>Z zF)D1qa%qs@AiUNDjw$hUb2|!zhQJ=f!Ln&gBWoivdSq^+AN6zK(jPY$qMPQF7<`b8 z=KZ(CmyJZYjkM@&H8ePZut9f&qq5Fr#h+<6XrvIiwDj_UPTyT8bM-VN`KiY8#vy*? zCu${FnJcIkiLVi+(fkWO+e84(>W^ABjMuV_6vNqOA;tA^)tj`8Hg@okE!DRnhp zj-cPm%jmC^L`Kv)4=6nB>M&!hZib7msiC_9QyyL;Xc-2{*R|2F_-hS0U<%~XAnux} z>u>b6$#jRGOhYmx1!+gB(>5Q>|I7@RB2<#Qc2GQ^j_TlVFrr_=RRfo?csfq`-3e;^TmoUho8f;p?BVQ7Iz5N* z{uRQF!gps&ndKEVcFdX@DYgk~C^LBaijl+tOcgHKh={cq*=VKKY%S+v*%Ru65$c?X z6v-=MUT{a|{359N)t{~_0;f(P?63jdaRM`h3|O3tq_yiKHSm-N1yMX?Ca7ZyblOQy z(MwYjzj~D|4%75!qam$>cg6kO5dBgx;@Y2m#>g$>K%Jx%0vBg9Rd3BCDcdV>XAkli zI^mT-GLRC%o(@z4#da90Ny&Ph6s%^4DO%U=K>-XME9mixk?XJ!+t8&8{eiT+(FCTw zmq?{8)K%9e?R-ZJ5+C{+47?c?AB8+7SXwbf+2i!nE--3umiQAsS3441|Mp(g!5mF4 z^8h>M@Q4Cc#T|(kGBwh=U*H>oCGOnMA)77hzr#SgeYPoD(>3`p3qZqX0Vfnbfnl$e zB4wjF@gDxjR-^t)MKIoJ=V0S`(;7MRP|k~nmbSaQd%>D@*U+bQlL%^dHZeytWQjoN6)vIF0_P25vYl(ogfD;o_)jw5y#DoR6o@}e5e%BnXNQlNm`r>1pbD# z5pxF28W9n5Ppb$}Xd*8z8!Q90yzV*W8-DDOWf^3!uNC<9CVA{>masKApvb!G?duGt_;J3M!dT+#0e~ zhVonF*ZlUDbF!BU)x|3jO00Udo+R6&2#l#1Ieq`ZQOsY{x{o?6cFD?;ivO0(+~VZn zVLRetDiCepU~E9{T^9yRni~(@`H>M}&=(+gI&! zo+!kHyM7HjWx`Oq1AJ;u*S5i{$GeX(L_B(AfB#_25$LoIW8nCeK-9LY+Ad-~~Z!nMH%_W85YI0dk-#ad#-NsF#!}{owGu z5T%jHGB*#U-Cq42f^j^Xn^Y$TKWIzLuVRc%?VilFQpVt#3HK=?4yWciW+@P(_%SzH zlfeTymk_oW*TKD=}8gu6$^y8&RO2snUDZE};2JSl89G^l_HVdH!Q z4Jz?o zb9CxTzxw6`l#FJ~>7}U=(jS<16Viv}A_0B2TGb_Pis~>~p!Aj06z3mv_**SnmC2Sm zV5G)AcCJt04~3!<(*v%lj*h*ftu0B?l^Y*(E{fS;sNM2!YqPSl`nHF3Q$;oG*XNbb zU1So!-fQvCyQie25SQw=!siB#g%Gc8&0WV=SK}z^_TLp2-W(l%oRcs>x{`?4e{RV) zEGY6?|FLMgxoLUR+B!YY_F(C=x)UwoBv`I{)TyVY26O@=rM0wZd3j4gvudD>(!b~p z2VL-D()_<+WhGhZK$_weW+8gc6Fc{^s%;8IM@d zK}=0e#k7ACq_-mKG!rI!ZAQZL9E(ODvbzj$(FcUTxE<4QjpJES#q-_8p8Y>XyzRQ#vF%D6ob>zNTtDvR<^Mz_;Du(-UoM)6OD>2v0@Z?f!++!_AvCXG;(jf|mjX89c^DXbw3dN0VV&JCi)M_vgTW z3S!tVGBTDne?XlmqlSK;Yo~5>&uT&_^2`0?ZP$TQ;0w7@ zGH&fYqjE-7agu955r?c%3y3fa4$hkyRaG&u#u6Qq>GTDnDo~_(*QI}a$4TS_>onw}Ksq3lf!`BRFbN$G`mIob+EM`05HIdvc@oj>N%Z*cfOf!ZCNFKNd&Ii7BBjG{@;;Z>E6fY_bT zYwz39XSuyHXH@D+@sEm!i6s*;R?f>I9YlYZB)56$Hl})Ut4XG6S|KbBriHD)1=|QS z>MSq$j(n#t&vqs7%5P-BV^zbv;Cdn`n04p-xex>49UQxN;`amaR5;`nTgp8ZrC!-8 ze#eloUXdzn&W-oS0Q7RP*;31nQGckn$|zp7=-9uXLeQgI$rO2M;@Ca8kLvjsuv9mdB!?HXUXMP=j#Uejo9O@L4anA6u$CouGW^m^S6^itf2(upItT(fvh^ z<~26-+FY~(zJOf_<{k0bXxp!qZ=Q5^D#2n!bPX9%VAF@)t=ZTA?1TIb|IDrLA2}u_ zBBmVLW6DVO5JSDxd-#o6ghD|uJOrS3@mBfQ8w6j&u2d_l;M_oUr z-Dto0P+&g}W(rRY6z%2{O)+x}k~kR%p2o#b)?VL--C{sqWz>4tN|mR-MFIlkgfA^s zC6PrS0R8YxXYmc8Y?AgHn4B%l-ZldM#81?J?>@UMM_00LWR=*#DFLs<@wFB?n5-XF zAA{`?X|0Zin#u_MTUda$EFI6txec|O6L>$pri0EgqC9IxvS4YRu53+Ifb7xLtz5mI( zLUw44V4~j(&Z3pKS>kC4zvF~nKG9p77y+xK%Yp9=D0Vzy^nf!DF*1(fodJ{M2n~Xb z55_fgxkn%ma)x*2+>VM>3KKONq{MIgqd|>djcdk7{qpp6CiKB85ieEA|x2%Fxk68DP%s%(+w zNH=_Ek(7A)&s%C$%HTkhK8A`HY_8XBbEB%ZAZ1{#!0J4f4%`w>lPxc7R7nCgp+)h< z?S^32#|r6|SI_(?Vn);9W9;rTS=E2$C11}-tO$SmR%Y1$>Ymj8pdg!#y6u&xj&j%S zsOWdgJc(ay{}BxfWu%8r@7-)o^5T81R`7VFJeJ|n)~18%Z8!1wzdj8yWcdl69*Z8e z7Mat9^$#B?`T;fud}=PDD>d|N41z0uBzD0P=JNA9+)tcDfD{m`{Lp9hfhE!ETwQJK zytgZVO^wUidC-LQxGt*GZPy#=ji(t$SNY(3dXSo0neHHV`Y-oA7S4{Oc3=*g8jWMwz6G@*>N5AG3 zTBRGyC5_>IkAP>==ORpiaIT>f%40gj8M1N+vi~1h-vP~a|M#s@i3&wj3T1D}R-%l| z$ev}d?5u=_GNO=`l_c3o$W}`<&-I_c{M_uK)kKuPc7P@Avb0 zzh4vCm5~ifz_(EV0UtLZ>ZYZEz0pF(XYSr@aOX26w$QPTPsh^q-p$%Ol8}U}) z_DAnBs8L}yC8(%Dpx{17ZnJZ2R;eD)aKiZs%MdC$-AKu@Md*Z75`WY^6YN7Y`(}Oo z;uPYjF(?uAFQXzB`Jv!YbxRAajV{ZvQCV7jmJ&;dJnl<#>neLh{apF%ZSuILPpI&e z>ghSc^P*S{d4mMg3sQ}ME#GTy{^FSnnus8=L2eqX$+)8k=L0}Y2;M@WD2H+fg)4;h z*XZve0!KDC+uy$Z`|(gHjPVGE1`jxg{H_nf`xY~e6D}lEVCfkXcHMiAA4b-1->91E zsmtjOo)D|Ks#@M0dV<_wLGbY5Y7dEf8@0w8-7lg%Pvdy~j%?LX4*VAL&lEpE8HuzD zymym=yaeO~u8G~N`po}!JW@a(8X6 zw3*J{`#_6_8=mtOx=bT5{&=*J+Xf69aYiCeCU=n(rt73}^(sO&nwSIql~Is#hdh2H zJ?)^nrr>~VF~uoL;rSJQNN$o4agL=6lK@5nKy5IsDk-f{5Z6Bt#fgoC1~kK1=!UBfxjY@`!khYHNm#G+9i|bHh$Dfm)P1={&Ffjy9;6~-L=#D1L^Lk} zCC6HrwDqUZvvn5?8l(CHUh$Yc|P zg`m{)a$sz8Gbsy;1g`y27mx8~mv5bV>A>|NV(TdgA&h`Psa96h_Uyd@>1!Vn3Aq=X z>+055(P;NL1X;$7#$`feJGwm3H|J8Hid;p8DmeSY>Q44v7K#6f z4;QEj;V~ZyPV@YEC(`9xAgMot<&M99d#tS4^}o*ae%C4arPN>$s1!;Br8N%C*IDnd z`m(}IwXssSw-{EE=tkBuDme0!*1pZ=`E2$9bK;OR($bP~U)Bpf(OXSZx$@Y1kZe5C zy?B^n;EwKlbk>4|Rk>e5`CskE;s4h*x=en{RW|Gi?zxA5Bx_UQB^?GB zI#(m@p;>92Y!*Z(A{?x+(&)g;%g+y1N-BuZ)tNnu^N(!UE`9UpLQ3ls`I^?r@eHw@ zcZk9Q!l5m-7)S|=Hon53EOb+h8Vd%HrO~NwYv)t93-m6#yB`2sh6LI;(qOZZqTK?yceAFc=3QV?G7#_^b z5JYsB(`4W}#+*Qaoe|=KgrG;V2?pVCOTZiSqD#@Nvr*}Sk#K@a2%Eyox;wk7UF>RU zdLA!%h5VShHP-lTYRylbJq)h9P_*RK0-7wFmz6~NAuy(+Cq1q)!)e12`+4(=_Ey4l zFMau``U20zP4CUg?GIy3y<_QL5%mG@I~IAsJs9SawK$zbWam9j;XT6-z$b9{8d`!m0;b^4@?kiYtr+m=ZzmhRf)Qn@kkcT!0j^^g8%d&E2@a zL*^(vKzHrhwFel(*x;9M;BI}vh9eO%Hdti|x#DQrbU>#iiYL{sUabL=I-Vq1yNf(4 z#zoovrY0ev5yCGV9`6m3WqNPZNQXYDjQK{8)waSH@sSJxP0Mq5TQS|jfj87}6@~LM z9Gtz@R#t{{s~wp|b0ZN0AMJ~-YDS)>Nxj)ZsZT`cks0|+0_hy`t{OpQ!s<}uU41bv z;>%SSc=70l*B2l7FnbnKMFg!zO>Yzw)J8K!zKq+i%XP$hV`=r2%hNEHh$F!=stgpS zC?2abgl^M8L;N(<;K za38Nt!NQ4i?4`7a=Etin7I$?etu%bF5hU!F!^J`nYM1mV@L5?xAf#IR?#iw?+~GTX z*4pV80#!G1HgwL!j(iT=+-R5FglFUK6*gJaNTMW&F&l?gSinmkP9CA;R{5F%1na%+ zfC3k5vgx&--og{5BXia`2vDEI#!V6uq_S5JUV!V7?{!bM=nvd^&1u-T%?{7fYP|8~`o}!aJzElyH8&D_46mb;O!eUk-h(U~*5FL0)Ak_gE2H|Bx zj!P165L*xe?=YS%G#H zm~@vFQ>Un*YaXJkhL!hn%`Uwq1(6jM#xT_-l~0X3^#1kn*%)>JR(4**mi+wVbar|S z%;N_iRPV!W25k(a_p1`xu$PN*u(Pv2PqQ>Bymsm%BAMSZkb zJRJCkKtof<<^3AS2h8H9N6*XcnOaNm&1?mAEUT$lx=y@ zL#YZ07n*>Up*{U{!ksJi)u$k=!P(g+Zd?b93mI3s0u9!0aYcn|29b>#j5!)H+C7Ja z4^o2kr!PqwEmHFeAh1RPM3Ybw5$r;ECXmgB{rhz;Zv~SSjLXE=7$~Z31YfkL>z0?? z!F?g(YIXnjZ|T+bb?2i9{vJX&*zrZa^E|hUc07%Cf9pQW`)j7cwe`!LUAc=D%jZ1m z$9+#AjsU?t_@AngU_gi!TpWv%c}y9rJNjz zIq!F!B)wdd)Hk!23E>v0k4j1=6*oqoa3;>JGKmlQ6L*?0rZ?_i@m9P0;d^}9EB|AD z!gk-Q!cNPr;!sa)igWldzK9=;r(&hHGPPi-?8e!M`ZMQeUgfrg{)HfHm9* zR<@-P>odKNR9M5LXgD4^|CUS>xZiu?lUW<^@NsHtbp!@8_2OApqXN8~|GSDE3@S#2 zv&RGh3M0Z}=Z#{CE;5Wn$cwS-b*p&#pTEzA zq#BVY5{Y2ukkJMxZRedB17eEDik=U_dLgx%AkaeIAN;@*X^#cOy!>)htViEn_$+wp+4tI}H{ z9t%lzomsjDuS{>P@A27MWL=$mD0y~#9xjO&Y+t%AL*t3c0}(en?~~U_)w@4owuu*G z4$}sZ(%Pyf1<(PLKWsxh+5relKyn3+a`vQ?~H3=eVl>|sC4FcV%l2y zF@sPd;z0R6`0`;JAw&`5PbRPj?mzPWgr^>c3LEKK&!5Dl^7_Ah%kg(MDR%k2N}4Ri z1*+89Fl1W-4ivOQX$WU-{2+3{G2;TAQ$dhA!P*QNG%%98Drp}r^bIb~^@bpT1dUwz z=2DErcXlh9IDal!gx{E0>d8KPgyDV;h(px_PF9#t%p<)Hc4xV^pOX77eDZus-?Xft-r7XWt2k7RSxc zGa?+INJs7zBZlC*fsH15L$AGC&+zo1_sk#eVQA5&`nkjWf{Xty>}L3fjA%dHup)>| zM&@}438ZE<{tOpfT+;3Cc&80bvurt|+7&YmQgUV=8wyMyZSsB#4P+Rssz~cH^ z*dXb{LNrF+zuWkJO8(FsYd)S4fYTl`SfrDh{3pQ#EoRC2b zBr!@o-EQMkWs8bv)$qD`4xwI1wnIp22t>&kodI@RK!FV60-7PN!?c7p92?DZ1qBY- zA%C{kwF%;DgqJ~^PUO2Nyo|S|vnBY(eV7jM)`)mIhtn8|)IO9fON?MmL-2Ol+(&jO zqOghL(0|i=8w?*^=+6VpL@sEO7$i)ASicb9BC)7M4i`C${;TS{aDXAO1tAVCgPfd+ zOw_&J)VetZqI4>a^)%UtoWNC2ppr4~Tfm=<7}k87LD@owS-@T-cn6>>!vhYp4NV7y zcM4ippoH^>L<2Vw;>D<`qZ&Bsoy~r%Mj2Tf!wde^Mib2&xTYa!L`m4fYSKS(Vv%uoX*FODU7l0tz3xQ zu6Ee|J1Zd8(+pj0a;p{O$BBs-F{E=Pt<)qrZmbYv(B~GbTbRD9kZM-_fF<%u zgjj7M!(thU|J(zf1}FY^cHECl#bibr1ni+~LQ>+S710hNqRW@0H0YMP#ShRN_6>JV zf7H`soBz#_49g+tj*U13F% z-)d|zH=agH&qQ;jo2)LBc&)UF0EZ_GA0RavK-R#T;_Pz` zudj}h`r?koV^4S=!fn(Eys17&Lh6Zzkm#+OLY&THjZ_M$u+^yVwK!vQSBl)+yd!um z{@T}v3|4u1$}vsq_gushAdLwU1iK+VIMJ@oGWbe4dohUSt!@}>5b{EtkyoKerb$pu zyTWEBjYZrA-+M~R`0-ajm`|J-pPsSEH)09qvFF+ycqIc-KQ@BS_+D|-3J*+Kt~y`7 z?uCBVV`u^T$2S6GP)3svC-diB(ydv;(E69=!14dM5*iV4?`vwBdzABJb+#$;EPhL`23|8Jy3T-X z5IK=}oh#klkn+xM&2VEGZX)ERfq2K;jv>$mY8D(HDhP##Ru4^^SgtZMNOKkI8w#q1 z_rZuPLIfhby zwd-tJeMAQd7lNPoC8o>(e&fF&nH=~MV!*$2nLFi$t*srxmX78WojC!)V+XL2xT8Tt z*kfbfA*?Fro zL)`D0WTqm;=qxT*pJZFs@;|8lL#Vd?yRV?Hfko^xa;6bQtHo2+j`|`b4}KPEHQqZP zeGhrU=^8)hFIm|i$@L7^!_Cz+HfU?S&SKe;C{0C;j8`R=6JVl~ET($|7yD5LgIWL} zTFI5Z7Aolk^Czebk;furdrYDFF*U1UkJf=%7fpIgbb{)kZ^-;jAX3-mkGV=V_V*k^ z<`!T?Lb8TG3(d)3y4BsEM`@APjCWku9P>FcX^_c}m}W*uWxq0vk{cbsm4%(i>DSi) ziG6!tXXd|kG9`oHAF^l!TV2=!Fu657aQ=QZ|f?tP7$;SB*zS64Z8_13Lv*PGenfb5^Ans>iA zKmIjYd*XM`X9#^>cPJpMiaC zAuQ0`90{F_KmY)Xsb6_M3f2`%6F$YxEbI5L{;K)`(!kPq5m!hEyi0F8e3yQE{R_%F zgc|@Z{^DZ0*50_&M^PgBg6%fBdA=?jLJLl~ah=KBA@+qii=aBq@>c)TRKX&a8EhRo~ck!`eQeg1?Wa8MPQ#-641Y#kfi=Bcs)5L3a4?X@chq zl72bAMbfHF?t}m2kSoeEKJWf(<&I{+nj|p=rwsDvR-y#n*%S9)t}#AH>TkBotymiD zyKUj0kdTl$^6fcMA-ng%>$BUpW91*!r2uwPMMe4clib_WnQT;t_&spe!U2NBj8St; z%x}OkV?b-pZ`iYwA8J>^lYQ9t#*I;YHRaF02t@0e^I45i#moQwL4nfwLkC%}a4-kI zWrLO$O~K6-l4(btW>uq>xw?%O`BZLGzxk* z39?sIe!tzRoUZW(?P|w`g{~T&D+&~mBOTQ^`JYY|-wpaC2!&yFKc7e7J|IMy=KUgI z=|%1~e{WlF{dGusg<2xul6V?;Y_Fal_?-o0sNNsI(W9izDH5@S2e>ie)Ps}jdwD56 zvQDoj?*@`3LRV#0Qg-M}q{5+trNyIBH!;g#jHg3u3Dn&p)u?~O7Acqex zSRHg-=*|W=adHbq1VlP-Up~WPMcB5gT70+m*mnN#svSn+1k$>CI5OohF>o<5fn&s| zgzrXoce>-{r8a3m$k1J?>2Xls$4Jh9Oc2_7obm+AAa9Z5V$(E!2tveXUumlI6I(R_ zvb$BM5gCR7DH`rLXsV-8Tdt_LGopS!pkU;w=wd&Hj-C*cf*jNaM8YE%6sP#hii-Q# z0g;7}XG8RbwO7$urpv&4WA2 zBgj));|JUa9iJF)4E1`C2_#{;SgV9+U2l>LZ9dyUXhm0i%*v$mbT+&>t`{3r6Sz9>N+8R=~A z-&|~2lExZ$^vtf9pALCDj@vzI%ITu*%__IOr`!3)_C+55wKk%Qv@CqZX=yI@qw$KG z8PC6n{+NB7S;AfL(mp+H@2AqG^Qw}4+?wxiM!2?|8+ZQ_6H6H^!F6QwK<)3z;Yz=e z2aSO@&cr@?dq%*mlwOKrdv-~dFZSJ8!5an}yPW7hH#wc4a|N>!llWIbfOFItODsW~ zUUB@PLYqZLQV_svfj+8rxdciZensghP~AOdFyUh#9L1Nt$zzcu4Zc=5P;_ z`>x!;Qf@p4)QFvaM{@)y2jy@{{jvH8))rr)x4Jmogb{J^_U+xSKHe$YeZFv9eYcAm zh?y&jk0*j7*Zwk*fq}sY+T6(cH)kYuosthc@Axh1JksA;Lu@sXeHlUS~(AAs-AW^X@_p4qc!3b^&x$&SWG4-xP8krUSBcqfj~oW|e7Zr_mc zdUOd&S2#f7q5kyk+wYj!nZBEo_7Oj13{~Xole;pbd14~VM!Cg38uZ;Lf|-xfIugmB zvXn@KoE{hPN=NS6%bRh*WWr~uaswg|ff!TzhWIGNt+7h72${9Wcl>$buvFpgmGqGnSw^zG7W^eh@6dSNmH@@$aLMlU=N(d@YXwgBR ztyPboXvLd9{Y1M7KRV#*QJGB{!h#f>vh}`OkciNUI>$ge+5i}5{6GVvdOC(y?c(Kr zVeZ>ng8u&CWrz_84l;wG%Ac%VaVY6vzHYDqPlUzG8~3p&xc7G}sJ3vCkx5NW+potd^dCHM?d;6f-xCjpi?p7qoN4j4U}~RWbRqq z4mBn(evT|X0C5q%$mQPHr$p2k##z-}UYZzfd~)m?d!EuSR>|TEGXAmnU0~uv%Z{p| z;%aUx2vm&R-Nzl;$4oZy*Wrs38qPlGWk0s?`NlV zEv57lELdJWoYUmc!78|@AYU5UP{`>+C_5fF*)y>y0YnLEj98;8PX) z_QN{adpJ0RByFb)uzT^JdF$8CPUcWG2 zlU=7zsyh9h5)lw6UeVuI>fWU&9bl9zmD`mSK!=h8X-2jE@i9o^x*v5++PrNl9a3^) zDG*bPhTm)p6bkLLi?5gc{lq^YD<+m<&vB45(%I~-?ZGO#E`|L=>ZSQ8Ib?}Q?%N~Cx_ITF>6WCRtrl#C2(U*BFewwjwsO%_K_R($^YmEC8( zX!~$MK?pnuh^Ejl(pvm^&k$jS_~p&*o8^#UhMjKeJ4sVhD%kbmrJzMtdf$zpv%^yd z9Dc!A%9I^Zxn8OYx%ig*NuQOAuQf84F+YF60jOFwp*)-N;>8ms7SwCQ=L}u%%az&W zi%m#!+}L{kM~){ttcet$iq!RFqcxY5UkPxYbmfS>N-IY;3DUK3qip$z_w|`H&Q{%F zAslQmj@Y8udJ8qLYP%12->{wkdQo50#eyzZTOtSS_i+Z&uP;cZPJS3@o4)2BZ#NWu2mhZntOBg zkXctSQSL*Pw6QyTOTJfDhS3<&jttcT&modrYZb+*@Ep5)Rh0xj(cFng#r2nk4x8b`SJxBB@h~edd6#|MGtdsj59)9#l1oMz?z7i}D zW&L~zOnUW7qHRMnQsE9%}zH@keL^!00>?Tqd^j=@gl+0|5yFV;7*uQfy+ue<6{Gc|R}vN=Fa zBt#YkG9kwgiNRidBzQ+~ZPlN51P>VsUO9cF`N=tnNTL&I`}Zj?96GwD?N5d z`^*~FuKa!7%`I%gn)l36iH$iS?+P|(TL7&iIu9Y5^ou`STIMm%-Hunu<6;hz|JqY> zPt|+K0YWSwNqO&SlZ)I#8=`C59vPPYex1cNLR0x3+WVbm_|~zn5qhy`zahg7D?0I| zFm(#E#wKo_;*VyoH*YRr^0z*}IkA1GUgJY&_Cx+-c*AxyC)_&u&@|}bNiXtI#3;FG zsq0w%Y)iGau#{mn72-sw>Ff03!da$6>;QxF^Ml*G>m5Zt{D>!~c)U4!_qj@%o>ORe zy&2wmme(bpYcQb*sjyq_8v2*4ZF80&>$%EA20A2XgbIlWNr#AGxWMowiaQd*=zv;vPz)4MvYimW)TJ+$Ad{_yA5S~L zSJdpxQEF<7op(su+k?T0qHw$F88gFiXb2ZOi)Kc?CR-%lE_{6Fj%RaX!t;zTA9Dn; zsswQX&yI1@3FK;?CGV{J^@||*#mt2HNpbj)gp#tSHq|*?9Ckuvh;QremVuSzdvU&7 z#~DpcUCh8|&P|&;Abe}fvP(ZYxJ0vOVr@!v+9(boEYq8Na+%H`*uac z)m9p<9ZU#RQhKHBonsIv1)&a71jkD+=!S;SbKB?5w*;^ELEJciGpvj-fANA*n1)yvup#Pg#qheRr#H6blFhE4on0JRHC^0%03yOvVv&CR=pk{kMbg}o%DD8Ny%Q8{0hFyP!0mz=Ge*pCn`!n<|K z*bmnhXlxuRv9a?IV_$Rp%6P+pqV5`EX4A8|KUw4>eF6kZV>3^`W7JsUxdVhNYV`&7 zZ0)|4_kH+~+@@X+e~CP5VM&8*R=g0{2D&H~osAWFZ=Wk263BhX_BC)kI-X1&b!{3P zS{stfm{S-d>aFZHc5Tg(OE#ySUs~q$seJD^xL?1>ZO2f{g#x|Ru3MD0^@?s|7`zJmX%2s;;w7P4cNXWut8cFtoC;_TeGb+UIwdLH^X$Q(;E) z?hF)qg7@qN-W#AtXFMipdDn{zur?taq&72%Ta?ZR(@TWKhv|Mp)r-~H##Rrd_oat8 z7eDz5P&!s%*est*biXFPA~4~tR!<3M@Z6lB_A{eg%-r%coq_i~KIF8r@yEVXOg;U= z^sCjke%I~#VkcxXl$Zy89OZsxbMw9iNB)TOp%b2@{@#5r=nC}Mn|63Fm&yo#?am)I zc+baNJOQ!fxpVnosl2?#2oYiy4iIU=cm*niinR4ZVdprMdbq4kkqj+t|JE)?ih7Pw z!YI-?RsK#{LkBF5UEr?2KLdzD$iGWV^_-hKI;i1DOCao&(r#87Www|jIT}WLiKnIVrnKs z_{1lXUW8zm1HdoO0XId*LSF}K;A z@y6JFP`Kj|6ruCMm+g=qu>1L|Pmtz&!~lE5$Onx7BCcEF0O_8;(*;EvJ))f)n>x(} ztG>da+p;N_+a{)hR&Ll=nA=Obi)=KutRtrGU7?n|{c>@!48ARF4~T_&Ra}f*azz&x zcHDY|QZ(CaKc#0)>7#Zu3z+7U_ zk-;9hkQ$quaXiQqAv)GitH*qKsE-_37$o230X4b}R(!VFBoFbsoUb*`oQKgb+Q)vt zM$w&csclrs8xb`Uz>_XUh(C%&>7l3q5H_F__F#bC+|@O8j$nt=j2xAGG#5=s`)N-{ z(GDIr_4G$f3x=HD?*Rpp%A}_HGev|QD6d0a$$GxCDeUIK}Zi=K8&KjGa^;Pb#j##6(?uI)kIpa$2?C z%QH7#W@TNI>6W)|{<&l?)q3=mE+wN-tSjfKp5mJ~H-*9*Mrrq(=-=Ro9Q){hHC|+% zF8%2R6cL|%oNpdng{ZQ7$JaK(oD=1Jbs$?b|Z+Y5JZXNY_xcM4O@Ztd~l1?cmg+4mG^d6l%qho@~#4N zp`dY@NK@zu!SLsZIl^yvOo{@~B%_L zuAH?4Nz2ZY+ZOW!C|N^RD6~{)2ZTro^SF@%Ey^!!ZT;WbOG#K1I^-somH8QOEpULK z$7h60mJZ>N&GEA_@n7Td2qFJm^tM+##A?4MgB|KyxpDN{WTJW_{QG z2y8UZdOxi@S;`evmxulUmDdR=wOQ+hzicT!dCHRM+_{W z^R+uLz31r=fV-5(iD@ruS1tu(aL^-8lSxeayHp1k?>rURH%I#{H-Rw_F)(EQX!M{4u>v!Fw$EtI3zJFQ; zCG*ki{D{kO@FNw3;|RiAc^^zTwlUEjIdT`Rg1{!;?FUGXi9Fh*P70SDc0#9(6+VnB zuI}1!N1~N`Z>b>_rP~yRBJDwX`%%CX6$_Cxx;iAFw@DDm;5MCsoX{(JHfS*A7aRpo z{l?e#G%gg(Xita-;QDOBUQ2hbC$w({9&YH@J@GwY21UpL%qPTB7gE);j<&YjXd*X$ zZ{$C*TvH`b(^`}w`Y4>+Etg&h)xW)r5;7a zmis`tdEa8=!c9v)t0(mrC@$5-L-b*0cHO#?{A<4vdnD_PBgF=vLQfR9-t2}w`O$lA zZ5qmhb*nvHMk7^s2=BGwvgKtqs*e+{FWM(wXJqJcFzXqv<;5FVt0{*py%KJ;|3(D= zcx-~9LC}ia${=|QchktFHBSOH*jos*cUIev&e~eO{QUEuJ{@_T@m9WRu%TXN(50@* zxmAL-CqdjjG}n977U<+QeNz)M$IoNcbui(h^&A)9t~}7v8qrWI>Qx;3K-l@$s7Vn+ z$?o;_X^cb$ISfFo1{MN>di@nrhaf1?Sz7whLR_%cwJomx@1B!m=KT*|nGArpbmXO_ z^_KEP?k0_c+KP+9e0f*OoV!5StXEX9_uZ=S*D0yV$r^`NJ8vZ!kPSaP|L0cOa{2bY z%%y7D{kGy^IR>Iq*4Df8XfGTpHb5ED_jfv47G9dC5@_4wQY!~S8T>kIMt*)6qdpj= zebIhKzu7bTup%Og@r){j=_K(gh$S%7g`zET1i;t#f=r)_yS_)0U2Hi0Jzc{q@+#kW zn``OCHoK2MvZ_Z@PXma#!gj&emlrExe-4+m-?M3H>^=FJ7&?l14Jp|I!A z1V2&^h-75E5U^8xMO^ubJ|&MIJTN)wX(kNu zq=4lo2moPyCxRFTO2OSkAUIyqW+<^#TjXEM*9Egd=*D8Ye)^bl*7pGeo;)2C$7zU# z0A@Uauh%J=)V#_qg!u z(VJp31s@;R1XMe)?kes#eBV_z)D6}+3gIdk|J|VrmpMuU6^90YyBR~*S9chL`CQ`i z)VS~V>^M?IB(}XJ$tAwtY&Xx&zxP~l(+nB)h=?^`SRHWM${;6tWY32 z=D0ImUltJ)6l4}Hl|-m=;sn~eN6}pEp~~Z z4H+sc23s%`sV$4JYL)aR+Mj4fMB>f1v;@yJq39<{pY{b zB6ZS-Mk?`Blli7+ZC>hnG4#KKRcd2kV|u{Hh{J`Q>)qT3@-52kzYfOc*Pch`<`+WD@W82Ww+OoJ`seO7Odwis!Jg0gP>dPDQ8x7@!^=2cGu&{!#mwc7y zB;H%t$1d7bW<*mLy16q|z500n+(|UtU>_&B>!Bvfcg@mGivV}j)@B+q3RjN@J1&g8 zW~#Z-tEDVgH+D?j0mg(U=Q#stW-MDm1kokaR`YuDWj=1aVeMRE$&MI;)W*no{ zrRQT3u)Z^(CHYZqYraeyUzm?W)@`d+EI=OLDiXb~%h2Z;i%z z|Gt{^v_k0I^eEsqoUmdEXcBma4rzeyF-k*DzFE0y}+B$;(K&+kOZc--> z4tr~tT|;?#>Lwo%J^h9AG@K4LHe8Um-<9GwnR4CZBqp=h`Fe69TyT9etT;B>)SEZT z9Mb!=r3?-CX=O^;6n0o?*!Af(72XguC_Yoo>*Ev)WJqYlsy8p?NRc>#GcQ6Zri4zp zz4TKW@UFi2aJLjUx@RHzAf`HWyE4ir$5DzkT>x$9yWNK zSqP&k9PyH9+FW+;ugYdi>{Vi!4kuy@nmQIDay%alNpi9$PDegZ8jq#2iDu#J%+&b zc-i|J>p^_tRm#kqWu*t?t^MN~(VuSyNtc zsp2@$7?=TvoOffa;XHM%*!<9Tz;;XauGV_ZW>w#{i||Kz!eZ<|8JxQB;jmSKEL>oW zHZ~dH)v{x{Y4?AMiKlv4yZpEL@{ML@W?^@vJnr1J%MA+`a~w4?t6b);wR0Cu^asQf zJ&9YtIraU`pH*cfvZ?ZVC+}E`)ASVI7f*aYIHncoI&@gGi)@tVHcoHrg2*vFhnFBANRT~#%a(;f$ z;yCN-N1DXc%!UVg*}y)Iy-K6OkcI?=!K+DLj|R4;Zxbd-P--;BbVOKZy!{v1!Dddh zoyDO$k#9F(gy6TA>FJMduOGu}<~Y`PpGgV37{ZjGK1-Dyy}L7;DF8}kBJy+ZG!N~F zR1gKeh*^|?S|iC1e0im>sQ|yj1NC$`RiEWsvc@DD5oUT6rR&Kj!Tz6Oloc${oA2tyj|&bkpm(ZOu4R6`(zokD)Fe=& zIbhOD-K*U#JPW0jw^*U>18(6YvJ*cQ!*YMGQ?<*8*Uq0CWl2EMor@n~8i;QuYJE+c zZf~3U^i=9`8CUk;13zbP4eUVh9|wN#M2{g3x6;Se5A{VH0858N*6zVHEE4B_E8*;-{og5 znd#O;CuUc6ZFOz0-9p>Ufc9-)FFn_7O|2vSUj>tpQ4zNYDMG!)Zo;1ppG3Z{;?0|b z>Vpr$@xfCNJDnc}-kirD{jOO|ymr;4xcf;%J+;3bpI$Xjs$H_r-2;u=3n?~bo}e_vmreyBeZ zE>HM@GN{k@T(E2;kO!*=07>I&P*nRkx@*9gv-9v)&BDE2){Rk%^w)t$zQb}g+yON zyVp@{JUu<#cs!r`iApAn$0tvP0a3&9z{QEe%?Oj0FnMXOO1QjVyyN}W6@1_a$S;gV z&}3!CYl3^Z##eYYf)7J@99$Qdeoe-uq*;anO~B0#oS){UmQ}1${D(qQIc`i` z0P`v~rz5~KGdG9DF zwX$zUj^!RkFP#1aynmK^0q#VJ z&wg_m{j&e0YK#sSz+q%HM6Iqe1YWYSc1zfw#9K1*s3}l)r$VZM^A&2EIFD+i_U0!U z4uQa&Da(%pb?Q)}gg3;!%vAWiF;!__9X8-+4^Q@d!OMf|z~)Q!{BSkh`5z@Sq7z;_ zu22xOu%5%a1_h0VMhuLoPnt;a7tw$7o9G87-mRR+gPfvPE#99O9Kl+LjEL}tXiG9V$z0MNMp9A1>0jGZ8$NE4pQ` zX`s)u!0o=c$s$v6{CeK6=$_-%1TZLIeW&7dXxFLgVo4DD1c6u4)B2aG_)v7X0y}Kp zCyQ8~nfUelEp}gg4{)4ev49O0;g^w6wmBEur9b^xUmM&G#RzVsy$8>hThJI_6mCze z17T~}Dw28SGom2?4Oe}X4*2p#y52L=r2^;xA%sEM6JMHo!_+j3yHj-VtSwiQ{?DR(NY zzN;=2ms(J87ZBZxRsyH2kiUF^swC}|1c)Cn@)&AemizRz0wi#TpO)+K7>%Ta?1fb| zlt2l%WhMDP&O1D=5lP zo*6f3pEwJeNymW7PzwqgfLJy?E8BxP>XYfk6CmaFVTd zzw@@E!l%dj$nM*rl({{`1NK73}(*osG(IQNAbs z|Jy0dq-+%moLNGYYsh~nb`dJNz4VR@q;=>w8PkwkWm zTP(tPr@GV~wlQ!^rz50%w>J*(K94?KVr&PMG0sw)`Uq`+C4ztm&pQ=;yYd6rUg(LN zMdiE2@NJwOL0pDYy=cTO@&dg)s)DCZ+nWhWDnVf_pqQEXJj$=*;lY!%;4F}zZ$x*H z9*)AIk!vMKi-Y?NGC2ZzKaG+m6J#wn5Tks5JjwSi22-nAm&f$Jh=&POvXJJXsVzlY zoOJU2v#NYlL5hp-amz)OKco#&VY#ZV=xA%ZO3JFq^Wb<03;5$}4M&`WhEG498cdTc z_k)gtFdh9ZnuXd$4w4=qNBG$!7dWoKJhcw z(nr>?Pl%LMq1f3xs;{)mO}sMd>C*szEl{$U)FJT23GAC9*|50t{{=jJc0mP<;qn?^ zXzh#2>DFGl;Na`BvPZN?d|}no(8%(s+>RrxS0p=SWFDk4PE%ecHmoR%eMH+ZGh3v1 zkP>1CclV`?mALws01FY`D8zLakr{YoVT3?pD1rs`IsAah(}{e&-{f}d#7*!MWHhfp z_DTo6-+{UcbiO%??P~Mi01Kcv9jR+4lt=Oj38^}-t7ji0F3O^}@E9zg=H?b-!VQ8V#yCX(_0QcxSJ!<-us@cVn2+83F<1PIxtvpRvH@9bPZ7k@zDuS98fI1I# z2#{@U5gmy{itUY-I_MrSjuCcqsvv}UUuHVP6ajo4s?0#ZI%aL)O&}OhoP3RTL1$}v z4tD6IUsBTmMk%`x(KR#uJHv-wOzORoz~Zg zMxf}H1myd#cF<)DshWq}yQ03ng>6|)~)_lzU+PTeq{EVU8 z);1iQA)8sMECOtiw&H>W@ds;5&&ckoe~Xo1L|iGv#86y(`&L#``YZWL03eD=%7b#0 z#5>}$!$D>~hAFLflGIr5DXwB5-JTzibfzw*v z1T`QjnGK%+Ip9s4@0(3TI@O~O7H4z;fX2rEVTdq{JI&g>i)(Pfe13>`+$$sN9UtU3 z_RDH|UX#knlVL9IBQ+ez!qFkYkX_ua#ByM{f1S^FftCJ~y*pBthy(9qtdT`mMT#0% z2_Tv*y$W1PExta1IC3G{!`>n4{XPT?VHLhCVC%rBr0zsHiUbbO*Umo9@i_3XT8`;d z)R!2g7w^r4COyW*m*>8l(#(5RORW78?O*Fm0AUH`Zg4P^D2_ozWuuR^xUXKm%#3ME zuGnMV+`Hl=2WJs(CLE5kYg76~orGk^23}ZN*t(1JkHpXx#Vy;Q1fbc`pQF)) z-lUq-a$gJ;mi$_?&h;w(z zsQ`6$6s8q|_8^!q0xpFUelM{E8(m@#m(fGG^dJYM#lR?D`ne-B|7p}S=$*ei1|nbD z)ZRYKf4AuH@a9zi?LV(toJz+buklzNr5qbO!W_*UFl1(Gx)1Aon_a~uCGi~{F6KTG zSc{M?$|D#&phKj@(jH#QJ3g|p+QOUbq11dC-uyRAYt&gB$`FZS8VI~}sIieNN0)>- zy1{eq!(PIFw7tu-v*Qu6hx5#F;P#2iu%t?iDksE$BVhOe@MC;p)6rql!}Tu zKM1c{eLnxfe>dL7GmxR`#90*;E1KIw8{o#ta>+IvN9Ty@FlEb^v1npjLHF^X{h<@3k6*i?VM0JqtFwoN5~i z4*5%Vx=j``G*((JHHq8u*<1he*gYxa#4BS)Lv-k{BQZMCIpcjX!=sZ#;=Hkj7!TKxls`VA z+E)=LLQVaG;1l0?d^VEvi;c^Oli7}MYYU>z&1|(=y(hWC>-{(1_e1iZi!FzoD)kZ7R7 zIqa8q!T1bS2qcOMFXEJ5f&V)cNX$=bKXppNZED}a6JDpUUL6u`vg@QIseua%O_b{n zVs~L6zn(|?r2ctvvBsf|oDlc-ZYultm@2CEKi4=`Ko_g@0Bcl9iTt4rq-6@9Q>CE_ za5BpkzT11k#JNpbS$RH#WLXcCe%;8+y?}c#Dy079qkK_COeBSeyjd%2r5hDmiOHE4GbsL)HE@QWP*{mKdh*nW@7OVe~ z)Pg$4fU8QB>#sE#_4V_kcX^B2PueDNMcv?QTAmA4b8Irnd1GUkG(u@+#hIGb+<+ew z@N70OZ}C;aK5kMpSmPxSrRTr=#E@n#50Z*Q@JMBF32fgsmb{RAHh+g)qSh05;V;CL zjLzIYwcS7?Y%44=eKFJdyYF25-5L0H;m_E*RmbccHqY;qIa?ChIkY@O>K|o&QDMy&(B;A2 z5!&Yi+PL`YW#;y?>r3tGS~efwJJcORGOlAxi|1Gc8iYzeV*lB(Ii$+UzD?TAjUMVP zymjL(mkG@X8SZk3jgj>Lk5RC~O^8+@*EKuZ-SL|G02bfU<)$;R4xb+z--1)AzGq}X zW(Rp97IWlb9j}iCjeZPC*jW70AdNxMyIn5%^Uq0B>*j|@Giee7fBroB&~6ssNwCCv z)ws7!bo}h?ez#N@4M!tngdot1ORR7ne=tG3j{KM2VOZ@+u2W@;&bC9Z+m&pUqerpl;V{fR%*=Ejn~PoBquH`HcHD7QM(&pI3<}C((HswE{`!Q8 zq*!cnX`dWkKZ~|!vC5{xKZ}V*r%x)#t~Y{LOxi?(vMaz}$XolG&}2_ZK`_n?%sMbW zh|q048tVV8&CE`!iivE~ROp6CyOaSH~ z^W#VLEgKus=dAwESKY77aD$mUSN2GW`K$T|>09j+dxGUQNA3Z+p*Uov!Z!JV9#jOE zkQ&05&c+Rar-vK_4IJT@NI8iNGjx$nCOfwc6TTY~x4|fN>GABDk|$SEYq8lF>8B2p zw!o)?l?HS@j+&bK(R)6}`J6o?{r@f{%P0TmCAtK65hyZXsRBY@zkHjYlxGf2 z>&$ogdMH@)0<*xjU?4ph$4Xp;zPvS11^N0UzGmMtr}lVt}%~=AFoHA zm7E_dp$15WUx7@O>_eFe4{NP9$%Q>qNk>8P60QJf{2Cbwznl_&!Wz}59MNvS{Tf4X z<#Z6_=g9##@z>Ue4vA^7-oU2E1n2;&TL_a7qGxCx6o#;xwGj5-2@msmMZS|&hwORC zct?PGsrFqSj%))yb&_Qmxa>llEI7GQl65b?~%9 z5XxWeL8tALyz6}FBr5EkCT zSD*|M^!>HR0W>5vt!~GvUmZO~vHYe_hMgaV8+9kX(9j_2Q zyiVU3J+NwkS`+VUXe>s3OGIwDkHiU5>8v6L^)d0)!EUB(eyzlMZQC|3geGZvKUl3Bj)t+f_Ts*kQh@&=)JqL>9x7yd+xz_vK7-E|- z0=-A>hFI|hC#Tj+HZQP8L3sLZlFCNrwZ?_HN#qB=vC`eUw&o1aN(9%X94p<0S49s< zTZt+}!q*}=YufbRDn$XvJ7zu+EP^>(nN>xurluyJu&|^EUF9vxn&(AD+Pik|ekm%u zL9)Bur{%cn=bfr{1T`|ae?#S7X7~6*_X?_?vx!s^CYOJe{GfKXG-xkqVxz$4ohyL%YB&*3? zG;5}EItPJZp`MYW6?{Y!lNuV-fCupcc6H^&sulRCot9-jWc<45k(IXgCh#?l5vfNG zQ}~|A(Wc9N?GtD0Yxe_t}gH1|7FDg)g1ZB%X@s+gLhb4a{5;CptkI z;mX8K!`QU4MfFy_9}B6J?;;c`Kr4pp9!~sto9R{#a|#YWYJ#DJM>2|5)zw)HB^;15 zrlfuN(D7_LDsZ8ioUyaBehD#IrSEBgEs!;Z>?k-p#UASCz?G2^pjntOYA=d^3i(4V zG`T}Y?NL@mXtVH2d->w~gMj0|7NWvBaD+SO%_xJ?z&)oUx~T)OJl=FLI({m z0Fn+)@I`)pcALGCeb?91&^jTh!uO&KL$D0`)Kp$1XJ`UVD>s|`j1v_!aPRlm!${-C3t-eZbgHF&2oVj3E;wqV-4bdrLRPMLTv zqvtZ+(X5%8oUC_n75a|iNn}#syn*@6^=!f=ninIqX_5&`Aty;8U;L6Qvb}YALJc6!y@yE~f4Q@J%;lJd?pM(xF1_Vz*Wyx8rMn$Wl1->k z1p78onSjqEycm9H!+Lbc#VvYlfs3~FrmzAJtI%uMT1ygnof#2N1AHJoJsW?K zLDZ(vuw7)hY_K-ryOJ|a{gpEJ5~eEb!#lP~7sS;a3O708|0`GFH6D6akfpHq5_(48 zl^P@77wvwnZN|xIz)4Ufgmrp% z&^b*7){)c~@7^YiS1G8bm-c;36<=fanh9t`b2JUULnOqOy@Mqg<5h2wryFW2vq!k7 z`eSW8hn;aI-@Tw@Z@BQMnUENgL1f+5sUZ!Z`p+K)dE55M&OV3BXJG8F!kAj(+}YY= zmQ>)fFInvgd=vPqFc83CH!&GKs#WoKXKUJ_(E1B!kDCsZmw*i#kP%{k?Z1+B>gQ;_ zhJ(WqDDR*G!RT|n$yf&QC{7lF5NzRGLZTVIFW6>Yz#v*vvjZGfj*HDbM@#riJPHv$ zfbD3%mu)!eay+!krtHlvEbNNLte2ig7_L*f^x5r)D+LMVkXFcPyy~9P`sJwOxMF?7 zi%u_k<++1&M3|qjKg^OqXW&$>wqCTGE<1L#T%O90>3Ov74QFRnw)6E=^TP10C43T$Pyz6n zYU*5niMd#2-*HQW1Bd$eG#W!ieZeWuH;R@WKR3LP#yM1ghx?x%{xo1?YTt&Y6v%KM zW8^{U9JpOQuB1}WwQ#&3MmP9c>qjBad%G2I8nD>x?5$(tYZ*p`q-|je*&SD7kzqAE z-(Ci87S2)0hf&#A_b85XdRfYUn zMdegZPWh?9d=vfaGcurj!MMtAWlFsqJPu`;6(jdE(|GvLOu~wxCaD5vJ0rp z!M7rY3>tBS64*$kUctU_QM;$Uv7Q?cE!nQQuZ7ayZ@>xkTNieCnDc6z51F8 zewbkhRX=AZg~PVAYxe@NKoO#E`-LAI5!@|rZm;}RRR9TlZ~xcEnc)h!{7+n6SgVoV z9~xRk1W>?*fCq{H247{H+xG`2W;E>tj$hMV`RJZg#Q7UAuAta5ma(tZ#d*+5Tz~ zaGwKir5i?-NM#KD^2G@IUdUt->WI86^bf984t6SE0ygYcbR?Dk0>)G*5yStKg|f&d zFT6PdGp6F^7xAFZg%|qzR{Z8J8iW5}Dkvy?Qu$v{fWe~61L&3s$E4usO|P%AH{mN{ zE)9Q_y~?M~b_#nImtkB&Mx%3M_H1;p-2ag4BC7w*U2cIuHxM_6jwujJhV3PTXxJ>^ zc3wW=yD(IaGR(W1wpUf{Gh>c}>NukjA4T5oOF7y&lOcCK85&mp>L#=`cxliW27{h* z9G$+uXak75s3%WeU8)Pn(Uvfu)1Ok~rxk={@C}f4sM@f)HU;zm zsKrgtJW^v$Jn1h1y9i|DiPwhRfyZFhI{l=y)G7~p)%o1*_?R$kS{+s4HW=|!PP(e( zsOjXWjsW05%43}q>JU5B`{vR~P{7XfDDHUn{I$t0^IRQ4s3a2TQ%5SzEHXfkB8=*1U1Cy$sN1jSHCRSIzPE33f9fvPaF=6fmQK|*RHr1r%dTZGg0A|3 z?%PU;!}8u(v>EmoI8(Kod(XF-`^!H~(BE;k0yWkFUJmuK5C&! zfd1WZfDT(c94D9$gAcIaQaJAMlL43t)+HkDkKz&GL%~F}IZ>_~W5Ts}&Dq8*X&DwF z)#5X2&sM)q-+orH6ma>CY0Ckh_k;rC6A(Gze@;$aM{AFjlvstl(blsJiFuXLoyWRk z#NS?O{2Lu~bdb04h`92VtLQV6KrsAoSUq69g;5k!*UZ-aUsQOfNkjD!? zQ+Sn!Pw-S+sQtKMfOHTN+HyOj+}t*0U)`0g26pt4@VR^r>Md4Q3fs1g0p$$GJluU$ zCgG{WU}cAY9U6N8seTw8`q|Re#b+s$(VV zlH%6t6wU1CNJ_BC7A?|K#;XUHAq12{yTRr17}}*^?I${aCUMaOss*H8XhbjQzQOHRV zguFy5DgJ7MsSB0jcbhb;v1nj7CNemX3xPib<;{k}Wz%cFqZ(bmy_2Y|+iqs{OD01D za0W4ifkf2T7s|KJ%XEuDv@&D_P?3CU=SS#&pao3Yn^ZD1R1FO;B9;>>IC!{W5J6Fn zj+n3rD370CH0sb39pB>h=QK2Mm(RO};c^|{+dao3zHAs77dN)saEZ@(Wiz76)>ih< zv3SoG;FGQI$|RFkODa6M1K;P9V@Jonx90br-_0Yu`CI1>@gmWL1QojIrplGC1@SEL zKX01UHWK!#^HYh5l^-iFsvOkE(5|5|RIaCf<0Qo+FpehX3X~fWveul`=ZNv7QyZc% zDBu|dpJ{1QXR&|Z<#Pu?)7R;t5~GM$z{qfu^>Gxg#rW`1s3}z zk9|&1QF4!MQqeTQ3wJ1x8OajS&fCYo+ANzk1V`@{5uG5SQP2S2q?!9;TN@GO@mOSL z#Tbh@cAef1zEVwf^+&&|fdPQs%*{8^>|-rHrsqI5X8NBi%H+fDe|lAiXy|4EO;sDE zCkKRdwx*mdXx6bCfCkXJA{NqdvmM@my`X4`%JeVlep>&|T=3hRG?4etmIYj7K85`? ztgpVSptr!}N^`vLR^Bm>(S`=0foA2SD~qIoF(~7(=MaF!;BS7+x7LgN;?t?vldLjX z$&a5AV%i%gZhOH}ut{{xK5;-s>JtQ_*uABqBn$f{v zk-;~G%8d=X;Ihbf(m#QQZM-f_Ay2v9aO*uE8IjMPSo#o*hlwgF4qrI@0)JP=?TUOL zoY{OAC9ss3w;AZeH3+}1%`I)nO`wwyE_hTm7!h?jv|a#2gksY`_aEQza2XU7iZ{`e z|8|>qXD@LXkd%-(;3xGsL+dh+*q5qvi>1N(CHr14hG%?+ezfKG)G^QDqjiK%A9EDM z74jTSH{N2*Hha*sfW#l*pB)`n-KvfjLr{XR3rZg>H%~CF;;;j6hO$tU2IGE2e1xp2 zX=ht`#h*W#tRp}BaPw#B{$0JPxG#-R)}hldZKUnj_cWcsYp^syybFgd)C=vO3%_JW zM;Apn%xH6Ln<`B5g+mEwNdi5unyC166;dl^8yN|iB1kR*LLhl{Hom`?;X69|XngM$ z8}F5AdBcoXZ&CMPV*9CnqJ7xWxfZm+b}1J%wo_mCw3&wioOINceuoT|(-FUPrf*w$ zrMn*gG$}}Au3?Qrmxu`hXOPSh7G_dE!Sdwt4t_B3Ro0!x~{OP;&~wZo~)py6J< zs-%*Z5lyfv`oyxMl5hLBF@|>v$uXEGoJ;nX^t&`}zQ$9iLq=|QCO6;TVG|7vBVMW> zKm4^~RsdGuj-UO=e;XKsq@?6TF~6X9KbXGH#d#0De$@tF1~}Z@qT6M~I$WJCP>;*w zQcNDffzEmJ|IcS(&FFKI=iD!P^pxh&T7Oo4Zt5_?VA`m7UTV-Lu2ofaDYRZH`$a}$ z{?yi(D4N>D&wXFoF}fVmpTHgsRYln28dlAce;om+qg*KT{% zMfy4u_QbbuDS!ndaSRY|`w>*?V8WD6Rq(nt3?IHa4r+*hwKEHR{HfB_}{2cIpF20f@o><`>ha+0V{-smZMMz(sh_AJ;U?_YJB?&J-4&=hlxhL7>FZ;&U2oTLGYfaGc3<`E}|rWvmPY;P~cnwFBF#-&l;mlE8)0UB)MnC#RYi&G(I84rS+-X)YPc5Y~(?Q!rty8@s{(wm!Z=E=?XPZxX#@g z*5BX1`*gGw$HXw+a$A0k7NeCvwAcjZ7n-naC%;h%(#$Zj<846f3L#c)F{(v~~=iztS zYgHGBUV7_vb@L)g?(uhsRYuYhmj7quW&XSP!4llyYy--ukaYsU|ir$e~7mbfUtv4?!q!-dhLk zcrJGXIxv!UmGeKl?SR7nVkLU-AC8|JTts&((nOXGm2D1n*WEtkM+x6rZEba0rjoN2 zbx)vzPXyZ2D6&YYhxRoDN9Uzk$S7bPF?b^pPn!fcLh|P~Bb>_r4%>IVys91+VTpvoKgC7g4TacuNgrKaMO!PjR3Obt zN`+cs{Es%jJW$oKOL89Kug{+1ouBfRf*#B;fR*9}XY3{nTGJ;~KBTJe*bX<@4KQAEI0jg zR{@fPycDEde*NS}g}f+in~?pY2y++8<&EF5sf~9XewY&Y3lw|y&Np%{XaYnW^OYBIkEa%q?5TTPe%f&B9TfY z4`WHkNgZ5rNNxAotQSfy^fmS)2 zsjCn>jsXK<>$-nzOOVw6eMj1lyn|3zz&t!kZEk2FWAtxG5S(cUKph^=IyHD%2AeLz zC^eqE$TPG^UW!Oc`+&@ycjEz;--qN0=SV-)wDN3W5`?T~`*N-D`2bB_ISmcY)>}JD z)=r4nk5G93-hLwdt4-!}5>T*=NknAoBIQR_{RY^M%|n*yN;2Po?BFmY%mqx&MYW8@O=(B z8}Ju%S8#woDYk@>b>-EtDg`+*HlcHdV?w^V(InVkCW}V0=FgQ1fe7}+=U;So3q@}Q z1PvFvFU>ZxRwq|R2@OgSq%G`(5)BERUzeND5uP9~HjimECYl7~1sPo0WB%0k?9 zCONRNs-%zLY{GH^aKgbs6l@>tK6r7z92NSR<=gW*H7}12N!7T7HvSr(Aph17X5|T( zco0mTkb4XF%@(2U4bP2L3`An-8Pu zEG0!tOUyBIlPmuLxtYVDbYP0ASuHj6uQlP@EGU@xeS86scn*%1YSa1~fR$0~m#)4o zpyu7tfkLZGgYaWl4&E0iX_?sb`qr*y1o|ZR+=e0C=RGT);J4bUKMju$=r?yf6Uh-^ zIf%U;6n^!v85ox+;jawAOKKK{Q^lK~#*h^jE!jMYsTx44LEUc|7~z15{dTRsisbi3 zQQ(0{WdW{C2+(9g31u=|F_F1YArb67kS4+Q;5K(vpR^@$7($70tqVHz@2W)LQD{+u0;XcA`%V-a|~0(0#N*@UahNee?Y zT1z7=8zGAo8%Y^P<=gHZo+}GW^h1ckW<&$XF@?N%SzV3Ka4$)?5)56znfq0Pskymi zz8ATQPOHBP)PE)@@a%Wx?t7nqoj>T;@oI-Yv@s;%$NlB5S2pqB?w&jptM79X!y?3n zkyug~4pdrp85gW{Ki-+;#o1bai{};u2o3|cElPMGo`l2>88punME5-mLQ}$?ZJft1 zyVfTpNl}nP@SgGIa;7E`(be|1kx0yiuP%x8y5irqw!whhOa?z349A^*|MMNW5Q!+)oU(vinX)j zDh!~v@)VTgkB{HoE{0B}+bHTh5C6}WEk>=fh66DpA+g{iuz|uVD* zcY6k4&CKj{Qh7P-^FKox#H)=WPa4=rv57yDIppa0NV!Lf0U_TdgMWh?Z-PI)qQ*dd zAAV@O`5@asl*%|gFBNSFo1nkCsy7DvZkaLJ}#;r4Uhs&?OF*-jf53y1?ivjC5v z^0;M5w9>JyoyC%en_*L020L8TL-zWIPD>_;02s-TdbhCa0-SaB(&wV3!H~jJkAOk( zX+wHBoBOc1{Y=?>^3mj*q9S0kc2h=(_Af6#94I60QV1!7dQ$RDxulKO=oOu=Eb^@aRaI`@F^7o z<&Foq44&raK0{$y-sysx*|wm(YVCUx_hgc)#{)R>wvaf#H8*pi#zq8ws{fK=KU?~i zuKn_^K|u>kx8v4deQrc}&f;?4c)6s8#_p5NPsEVzrBUN4T#2_5yndo4d*S$TyD7u0 z=U^jSyM<-Z?tmOz+pDE=U%$^1!Ak!~Ttj?%#(iH z^GFLwDJw&SO7qMxW5^3Q1G0Oh3I=rI>#S)^2Tz9$*{%m(Fd$t=Z-B(tAmAHjDM=u{a@z9;9h6N?sR;FfQ1s|b9rs-PdOJP9w8-bSs#h5E z2hStBrD8$1|5;eSjvl{o?~II(l|a-DYh_A3JhJvhdk{uN=*&pD}a&=%tt!fA~n0QlvqExYhbun>~)v^vW!gzttZ1R_Hd#*J*m#0JZ?V+1K1 z(Aj!&Xk1_SaNiyf5x51MOKjRVUz@sZBe4CqJB~=?8-$eDyIzgieM@K(f6ZMuoe&Cr zTK=%-IfOoAtB-=m8D|>CPM8_p+Z!VfrT@S7oyh;fy0&6w09FGGAtj|vw>Uah_J&)Z z76mzOKnz%$9^KqaNYx$H6T5BnmKw{Zi1&q}@xmTR=uk<>6f%Mt2lU11qD2JLVopai zCBzi*AV0QP_SDiH&#n_8-5wvCjM|gOlVY)i?NCadP@Ya6T1H1pulxHM>GD zp2NDuZ>VeXFf^PubnFx>JdVs2NJy}KV(H2tzi{W(D+ZtUZWBYU%cj)-^6H>+M3Xaa zSRlt7HF`AEJ{WZ&JV!3QfbPj>zTKX{&&z=*pDO1H6vH#rCj5~es%VVBc@)X^q zRu{m%52WA8u)px!)E{%nwf(LFhhP9+3CuIF@Z+B20LcO^F_F8Klk+3x$rn8dIkR-X zuLfs8{ru|9uoIC=oe1)BjN5+vJDx(2QF-Y{&xpFj8Ib6ssg}MjHviu4ZA%0E4~E18 znkcHkUuwItrcX(gY0Wn9-9wN2^Z2eaQ0YUV*4FJLMg)f?++*w5)&E`;At*~ANHLVj=zMF{%f-x620RlQmTtQ9@utQc zG!x)708FBy8O|%LxvgEAccD*=rh;Lyjcm1VE^EVMxgw&)DQXO z@fXDN@E!~d99O^hizigY$iCN-$4i&WXgwF56NfgUbMQ}BJvn2;HHTQHwk_aE|!kiKJsNI`g@Q zH#9P++ALe~8=9?*SW`BAUlr>6y9CTiP|h*{L3w&qTFkafZo>XFV&q-7Q7xmWeli~rwOs?(f6%q*Wm zL^In-@`{Cn`Ht(^X3z|$ASo0d`VibtS`jMFyPtj3&!k}eob`m;P&@}_V@u6>S=Ye*x*F$mv% zm!}>p)4p1o@&5E=*6Unff56~ne{&l=sJLn1Pt~|6CwY&RV=P8OW>|ry4&d%hWDp|O zLtL&!UvD0{R9?@7E0z~ebLDf1GH4YyRm)jEn6Y7)MVv7HntbKQfg2bz!40Qxp z^)`!waz{0ESk!kQVY2zYt?YP_-UA%ch?P4cu9f}_Ef{B9|4tmoYJh^nLo6~{R3yF2 z%6gm}9g50tqM|JLckbThxLP0IR|lIK8c$*vj{l3aLvTym7oFzsX}d{UjwAi7f1^rn z1<4#jj{r4FEi%+7JvP<@YhDOKdR@@<#wp{l%^w8q6!OTkJ_B;B1HN0)VK)r|HNL^S z{YW|02u4(_5h&Y!DH_pw>mb6dK;J+ej)M$a(8N@JX75L9o?C=u2HIi7X`4W#)Iu|g zfbSna6bK0Zii5tx0qJ`ZRnx1@66JsLBCh^N-j8iLQ5XvP8EFRSv*_&9BslVK?^7-n zy+E=U-x28IjS5!en5D-p@B$&Vtf{4x`8n!OCP+3*Zig#a0=GM*%a!`iFPv@3Ub*r@b8*l|?Y zhGoC*A>;q7;V~T>Lo2zN75>`cJ?+p$}kSe~}2ak6N(wbSf=TW_^Xl|h5Ea9Swu=t+ zhHu}_A~P6j$#6~$JSK@Muf4eQ{{AA`m-ihj_U>vZZv*-{6=+_;nDSUA+sVbeA*XVgdY5mZ zXHihl?VsG5r8cUnyu@uZ^VwK@OwsT!omjv5jOTTFrn2)VMGLb4;4|#>non|2%xRo2 z9X?h9**zF??8t+*dI!n+r%qx!zTo)vW6SpE&nbqVBNrb7H?ax_+5{!X=1+LT+BZAp~Xthl&RjyvcQ{$cqqELetl@STEX*v-{E| z+Nq*cB5m>}rkbADi1}!^MA{En3I-SumN0sR0HH^V5z(~T&acQ2%uIb~Ya`M8l;7ol zk>1pf_abhjkb`z9*Fi?#5~v3>2R>aASNxRqQGNYRc800rmmk4xLmPOt;VlqZgmlX? z1dk!T85Ao#I~CVmw5G1A;d+RO{s)cVEI?^t72%&PD6?WgLl(~IcX!Y`@gZ4}A53e> z*vy+4+p8SKcyeF@!R7+SgIq#JgwD{eynC{f*o3r9Ys>|q-PJj`JcKoYo-%g$xA@Eqoy@d=HXwDmf2#KxS(?g+3_0%3UU=sYLN3KO(u#5XMiNiQ5LdvBKdsz{E(ThRMo~ zUJ59j<^L~KzpFl8^xxaHMX$)-x4%TI*{yxTk&==UU_VXkr1u1NYNA+IeD1! zVdIIjpYQjSuAeO}nLgj3RX)4-&#yE7YDM|Iqs!yowX)}DJ)+m?q9LVg@!f57e*xQ( zld-R#6fH?ui7l*lItVWlQ!pz{cMjq66(rxBNP$K-tjtxnI$%dufr~0Sqc@W6jJyxJ(4;IYT@PWL7kl+1h-$~2p_?X71TmgbE9qc z1EU91_pPamVsy$XI&A*l7$p>2nwr+xIH|;|ge%C(2AK85O^DYU* zf@JDCtH9xksiq1JH!4DyhBP^N1_)jTI0vpaGYj@qZ1z7~)mTSBFrAt?t%%^nGt-uX zubVdkBF3n|BU!h{To5@D$%UV#@GXxXH2uEp*{ab9h=C|!#u#?^IeJ4M!)Ka5 z@#`i0=4B<}yJ@O>q8uEIs{Ww)qb&V-&H+SE+_f7&$Q5IkH|DQU zQt3OaXlQlMv<7+kqwF)svNu1_-@p5^qOG(vJ=BW;7a%Lt(dAdy$nXTP6E>{c!wzV{MEXBF8C1`jQh%|y5`Tr6ur4=`m7_KI`bFwP98xq2a%>|j_VYt zjSpfkVKD(5?}y!CpY~qR@@^hj>mP?nZ-XLs zD(WgL(U7}J9vC=k*v>&-Z@oxA)06n&>eGf&hZTt;j z!?nwS|VVkCW9J+2wv&=(n8|J>+1d#Fe$9JLY)$ zGuOcgrIZL-dr7c? z8>Jhv^slic?=Zuk4uNckxvxM5H-f7GWs`wwN$UKn(}(ha=qHT9)q78{gw~6oxeo$* z)UklNlVjlh(3`02TDO4r;!u|*XjxxITP8r5k z0PUr}tj-{adQqx`+1yB?h*pVX#boHSvgQgZ*-rc)59rQyvyL(MckzS4sl*}2O zcP`RL?BY4-*E5+ zu9I6&in6gwXEzM)fZ1MH&ox9LS;Tw&lycnJ`ka;@BCfW=kWH-c9Jc;-b<=P6SW23R zEQ$?#Ut6W-QJst#~wB1zChYvi`(pyoa;~Z+bQGxu}ed#pho8q}2Xl6iZ&vRUo_u=S!4I}gt_B1jrcVwu4`&5Xp%Orxr)_rH1zW)RDQB(XU|CThhi z>gqB~2N~!LMvhIp?zZvh)ivlu_PeR;jI=5x8$0{8mbisjL>Pwq`jP=qL}tNBf5~ng z>F#p;QWQz=;Rb`$%i2ZGzx(cIzR5+|(;SprKQYSgn_F)2_G@mo$}nK3gLx;*xdbBz zqSc52`;c-11!Etm9&lbLi9P^PkW3POfX`({*6Kp+wyR5KL@X=O{c#-Sl`Xu{7CqU+ z6hOiAx||Nfb>bZxL9g%4E=dVyxFcuC$;Q%Dn9`7<-Fm1 ziZmgSESWMNjx_y?XAd`AmP;GYU0x zBXGP!JF1EBH|PNBZ@eW;3V5AfKN5>pO1wk2r}mVrhE~Mh3UPulB|;ftW&J9k?4Hzu zd@y8S-bL7IvYRQ+0Y&~Fu(81swmNv?E6iYoL=M(AtoHRc+-u5pv`k08No31OJ4O`Q zimakMN&zz>#z7F8Ul+2<&}&cnw(tPvF;!&2B1-}_Z4fN&dN&-j5n^+D@?rI`xl)Qw zhdC#Ne7?TJFlX#v9u{9eH&r~zg`<_g@JYfSv4KE^_pihWOBl9HM9R4V3&9`-u4iI8 z9%4F*pn$}I*;_W(6Li>C-stRA+*@{+YEmin3h8;jXjjoCW4lX%8V4s3Kn__h z93n5NCw#ZI?ky}`VqlqvyWX$rz*F+J7{*y=5i*<#ur^)g@NJ}enV9%HrA7Pd)o-xs z8e?~fhkCsN)IYz-=c<)~jgr--fz?y7v9SfY9I48&DJ33@#RJ^jq@ncGn1cB)By_d@ zPwo?$Jg4K{^k?Y?-`cA&p?_6{8;$?7GW48`-&h@Anor$$aD$HsYF0BEt=L$sSi8jC zH5mduJOAfj&OYxc_Me-l{`x0hc*u5%kUN4j7cMiavkIE#pxLl*{t!)cemR-%DbS4( z?QT`|>ohkqsSRkM>FGFIfIW15Ilkhe2wm-jqY+;uE4znN;wKp#Vze-QXl9Hcex>iz zT~u$wMgd=r=EwI6sWtg6(SpuVN=8b2Xk0z_3zN(v0FnQt@`48i&Xks(zDe!zlYOmy zo|l+fOmbEyE7nh}|7B8KMUqD~2ng{ogaKQ&S z<%kTzxt5L9%ikTv{!H~ZdJL?Tpjg#Lx1B2T5ZT@b7DMXIW`&+7IN`_tqEq|nX(&iT zAzgZSm+^!V`H|+r3d&@Ur49-^Rk2apgGw5Ku?Dh7Ua=ntR>hrUiR>QalK?8wHUkrv zr&E+Zd?`tFh9Gpn0}f6adLIF(4j>YK9|(Ee5FdkLVlx{Okzmfw|7cUIghQsp=@@mBxXOb=%m-X^X;WMqW| zd5&1auN4OIFz~z7?Ccw6CTWjbTesqXj#Vzb%iuq9z_a|(ya8Wip>=i7Z4>kh@gQsV zPkrBp?__P|SYN{6?=d>WriFkr#M1%XQjwqef64u*n;aRbSN*$EbDPM-vq~GGf7m~T zN;J9gbiIjBvn?3b9(9`EfP@2QIPBB-g;J7}>5|p@Qjo}ePeKwvCa4F1mB>PWKkvNb z4h~J668M||y+0s-2OZ?)Sw!j6d>pd~va*>*!DAqiP&M;CX+Sc)Z}n!BriWEbmCmCa z%A85B4dFmUOpZ5;A&1MfCq?bZ#Mv!ID|shJ#y3WZ+^ftGagTp5-?nU>OwS3$4Ov>5 zxS;sgP05N8JdsNHCx#a($blU0SJCe;68Ky(oU*w+^J&0ZWo@;1>%uTtF*7g|5Kkei zo|Tgm20f}2$T#7xQDT-Eu%G;R zt0T+4tREF9l4$0Gv>jLu)?X6@XCsOf3rM|dxHpI!OMeamLLc3k&{rR5!b zm6EvyB+Kmol+XgSqft3O;EAAW)5_9lIjE zTY0n~88pk7IcUg8q6%3}WO~aMA)V$g%MC33jvaHt+YFGX-@epIR4kSj3m{X-=TDQ+ zK0~4i70wHs)kxz&fqe*5!59AF z#?VK3WcDKoSAt?Gqv9`9SeQo|iKOqM9i~9V$@$PuwQ(-qF{dnMC|1!7> zs=tSz(>ky)6B&hR6vprO$&%rR2UY=GDNT;0%=uE_XNuCk1MX)EnaTblPTRV_ zZl})$!vV6I8V&a93Ocs1D9MLY!%+0b6qNnSj5c`UF;(;sW?ON19k*C`Ra9*3@>tB8 zrtmz6mcpLwq$jZIz`+Ez^ZQTA;pL*4J^x3L9k|-_kH{60H&`m+eShugto_X9%a#!x zZ$LF#FszPJkk7Qp7*)V>4*^m#bsyMr9G|)e7CX!}mOKxyU-wn8Rm3H1m?OJ|i=4HnL^)-1}>H z*4Mp0n()WQdY6w0&AM0J$njDCm<>bRqneOAA{kTgZ-whzdzifYA_xt{V1fw5#KDaK zj&*)0+pqCdLZcS1>r!!5k?iA6PK%xN&ZV80&rI~@K@JX)fo+jbo8<=# zGvJMv)-o7B0$=u+>LKXhn?{v^Kyf^FI7v0&qMcPVx680u9ve<} zw7aTlnxNa_K)@*==wCZplKLclCpqpk+R(z$PPMn*0-;d^6e7nY0f!Z&-g#mbgOy{;6Rk=OewG1*ms?NK*ruXpd#cnW@h$lKab!6`b+W%(#eJ${i}& zX5|x)o4~gc`=H?H^?x4HLlP7K=zs`965jLrj@oG!6wRhn!t0AK7#}sNboh?U@D9a? zZ8YELT8TpDP2%g<;B{&+8k1&ddy?(hM7&nb@~!)erp7<#TkXGmZlPcIAQ?Q|zP{3W z<({CZ7qgZOm<|phkF?UIVkPrZ#T4l%L^x<@0@{T9SfZl}du_7uIjucmL@2hKknH~? zrDqGm?rq$+Qg391dG*N&AVscTem`29pDNr$*C$kttb1)+{_A`o z@(9ty!uIyhBvUk%>!7cM*|z?vekbAE%GP6s($Wd)e}e-y^c~2o4Sv5S(eyjvX-!Gt z!#qAWw+T_F&!cA5Fln!^ZSM+X-tF_Q(uXylBYq%k@B94^KT%7d z8Z2>;zRgTD=P@qLgE-O>b;G-l%Nsekayf~QW z$5)qGil)o=TbXMgJ-Yw*z`FOJuV>DO+&jPF-SQ`^Gs~UCqTjwSb|!*fs79!%R*3vc z`OZxTcNlsx-KE$pe}_Whpj>5y%I2NS6j6c3#&xv|mIKS~+-#`^nm0YB=T?k!87Fc> zG{s`AewP|@S15NuCI7Lxx&Q3S+4ymr^5)x%Yu;pOotpy#=eC~B)a0!DA}iwwWwns? zYe*ehCj>xpC$?7S^4k@S>#dKP##;>9da6RR*o5FLvbz@^haB8}?3kdNZx*|6?ok`P zs^vN3`FW*Joufk#&(bzEWr8z94y@^U)7V(kHgGQc_B5zl_eb+qZw^@bS3bPtnmDKNe)*AA7y6sO5_#%TJ}#&mlBJ%)s>m-JQOh zcV|9OVSQ9%r2Tuw7mV=yALSX3ip7VkOLYTdBlY)we#twL(^FaGP%}9Npb{VH~2v@W|$`CB| zKKYomG&4kl-+$7jrqoKGJLO8=6?HT#oub&W#Htw3nRjdjKRJiZWgUKw5u!;|W{^HwHA41~r=-3(BndL+T z{N?J@=i!7H6K&0MOp_maKJS2{c^IGssTe|!#O7Ec*xPY<>ae6_Tq#!}8nHnqh1NGX z>^|YOH2?OAq+1c*_!Ys)E~hshi|cMd1qCiD>LcH<5i^51Q58*$m3uk*;J2?zhOb$| zCFt5}QurfZd!FqyM>_U#)%$*aWbS3*ND}aQUwBrsZICBPGb6S8lv038TT2mNVo$}D zboJS{m(SY81s^ye-1kXFR<_A!nAN&4!4IQNP>#cRdFjf`NB*GM;Gos`o5#BQN*h&J zCFK3WP;TA0lQ4-h?Eo*0Th{AmW3g*vb-u{FT#7^hU@2m_b>DXqMjS`ZhxV9W-(sAs>CP=k&G z6ou0wcnfas1-wd(w7*8CN?c&Qe?Q1GZ2OUBQ*SRj68!XvM}{U+lTE(Wm}pX8WC#v7 z)yYSA(@-^v2iDE#MI||yYpSB78p0{>nI?5xpo${QxZ2D6K3cf!ii`JsYJ2;ZTabS;AUl-#KWV>5pmU%3o06& zA5$)^XsS~VIzJqzk{&7l4s>;E*j)E_?RDX@&q_GKe?rY6An?A*sw&2h)*!x|0-Db6(Zt)#S@QB&3)+-Y|H*acbH zOA#7&UNsW!xw=F=VKRJ2VWy>&8m)-R`qO5Ma6Yal)s|NceItqvo;37+VMJ^MY1Y_C6qPD?fT zKuN!pUG~n0Rgc@v-kWzeZgw(D55s|vFKNh#A2)}k5TjDvd+gva&)@Jcs$sMnOSnReynOLpWfVwMh$*lP%@aEiX3>Vw8 zEpt^gY;3nb4l&53&`?VF3}%Kt`)92bIapeLELie+hZbGC*P4PGe@TYl{8)|ygY;@D zC|_9IvoCIR(r}~B$B;j^8PypQC>+mqlv!QTm!u%apEeRq)!#pyLEW#ZqN3u;nIJ+& zMLdaK8LTg7x<_|G9J;?XjWPm-bHNby-t0dtN@tRia?$l{aE9~gOlk;WQ{#%|@T9(K zXwJ9VyY-xnIh!0aqnf@x(*Q?{Guc>mN=%N)DZB`(>EzgxxwP&?MHN|##41ZFDjr!_ z*!SRJ*&a+Zhi&k0d{@fxWNd6)eiX9$wlnx%RqdEgCtr6r&5t&TXudGLKwQ7SIw#I^>i$iY zJ9#iI=Y%^y*<*zh@{gN}RiX`B=9Tj?btiv&l@?cA!+2zRFne>Ha<1s_RB4!M+KEt`0`#g|8iB$%Ta%6Y2CQA|n-Zi%|$tZ{B)l9RTFheLgeW;>8K~T_hTlHpj@L zMUn&)nxI2bhJNi$!v*B~PQO?dJCvyt>mAb=7%wi|u)3%{*KqOQyPK-9UGCq1=;fie zTUSjJ#*4~LsXRO^npK=-rATJ%l;aWs`&@dqNr}eN>=PmZNy4 zx$8+ZLv8s!fA#}awjzeD>*dTCFz|cE*}_HFNJB!vl^Yvd_qMaq#jOa@v~v|c;HO$& zk5)6>0;+kvORoxVIAq*c6!gCw6gxG97-);UgL-){OP<{bsfoz_8b%;#q9aLdUsy}Y z&2yaYY4fbxK4%z7=B)@2VYromNapC^J% zA>%Le-kYYr4{P==e{&c2&^&9^>cIDQCq!?&9_BSEG)N@35j}*4_n_L^{fNmdy=5>$ zaa=rlAlug)oyvlM*WMCr1_O;H+jlcjC_r+A7p3FPsqD8{rV0ti*dq=mdX7-9EfrXt z8qcydk)63yybM&HMa^dMJEe*IX{k?=J+o%EwsT_VqN5pR6Q-rk@kGLgKeM?seKu2P zw(ss%)fNwq2Jr03dd(DcjK>Yx+fHG{qWaEhrK}}n`)~5Xjc$+aSg^Giu{Q%0|%pW^QAF=97-W?T&*EMJr3%%c!mO9H91a#Xc0|33qS?^DoMVBNDADrj}uoBBW|HjEf z2WMFWFI~ReP~@upF!>=~D|<1gn!|Cf=KuriX5{gy%CQC;$eb8Vyo!W#fKb-X#R5f-2tXM5F_XZU5@uS*#mHsdYo zCLbruik|-MDMjKI*Zk)=hGP-RaP?3I`W_YEGhOOF>Oz=h50!GhS;TrPJTUNeNppH; zriGVRd{&l|RH9>#)rhcfC&50yBWV|0Ld{IwMfpC78KO%Z4HpmI$M%ZYps2x*nhSbC zl}9&pbw=PWI1ntr+)Jw%M#-QCl3M<3Rb8E{b34isvhJ$WDs~+HV5ghc)z{r3T=^}< zyXC-Dn#oD+-!4qVcV(~VZl7|(8Lcd*4SfJMzsF-AAL=vV9XN8n-4_p#a)5Ton!9Uo zOYgMtUmH!V1O53A+5YXi-0M1TFAX|yMhyQ*XGPQ~Gy37vT0Tw|xW9F~F|6H7J3KP% zS?b@TXL9Ic{|BihhtBu+tqKo24ikr^A2JVdYqC`czP<5ssbN+jBr)1 zTmf^X```F%9F2Irn=5ndgM*|P81NSH6_w?lpmoTCl72Wx>SeHi<)nGkV0i4O)P+w1G5Tp$k zdxix~%7A(mf4!=;hwA3g&R#D*H|d6awNa&Qc$-2FPXHjqC=^53_@FPdm@Yd^jLA&b z+SFT33EjMxZ|BZN@iB0qoTTy@`yP`ql8fvwPUk75e6;+Ii)08BFK#S+{lss+3(XQ> zkVrVy+XCa1_`&2fo8)I~30?V~E-qu@w|QA(F1B<_%Si7W$miI^8g??~&AQd&ZhP~I zwwoAj@uE{U9oEK_bm^)5f&ZP7j2HQ|R{Q4_(LW~*ZP>BfLz_1T4~c1NX)SlXtag+j zoaocF|LMVbtZ_qy{X2}j;6}VFn}Zdo=Pqq^@E7FyXLoQM^?zc`heYgcnL5?!33@rGS0{ngtOB6I_~wS{D+>do7slE6Ib>Z-z!+`vg4bKvJi&co#UScX8iHUqbvo`t5yh`1_K z9f=B*b(^$B2ind1hOYTW0yMw>xbSm-=2My~<$3Y&D6<;!;Z883Ia2>1)7;~WFHgiR zg0w^?fU1)e7d(R-}-rgXd1%$^0?67Do63 zb=*uzan0v=68Mw35!(@s2U(>I#p)xtn+gxmzW4&KPK1bE-Ke-+f?EB;Md`662Eh=g zWyfj1dG_dmZAX@6*{P+My|VnGlH&iQygNl~wqEs%D(}B9GUwF{xj|LP;q1XXZ*;mz%=K#Q*jmEUBM!Um7c%xjx1C)0ZKG2<~O3?1{xpNao>K1K=>!2 z8QEMb{~RPJqjri3Cj(3M3AAXIqx3FjWMtAgr`{Rb4Ez!b|5%C^5}K`r>wH5Q_vnk> z2xm%Kyt()6S!2LG445|!3^*XDfQKtufPeB&Bi)@3-Tm50#m57DX(Xw20@HNZn>DVX7B_a}j)TST~X`C;UKZYo!#JV{IKiAxaM*&4-L^KHlUzcmN%b*t=*rMJH2iI@su7AH$n zJB%1h-_Vhf2}=F6qZE*a<0Q@AJO)fp+0L#b_dP#D(w+x2Pa*??WW>1qT3f}TYI+pB z0?7SF=eW4b?~~HfH26b77v#Ox_N?8`v)|u7gk=(LoO)q;UCKDsz(#a10V?BnarrT~ zwb|Q(IpN@8qAe~Bhz4P8@-R{nu|@&|+4K=ZxLFuDi+84G=Y$wj@>)28;tOBdSXk61 z=SdhC)MM^P567i+I2i8|uWU(oQFBU8uz89C3WsC49ps6Kabxjs}$?pr6^ zELCq2Dy<-qfDWoml!2I7dPz0>)VsZUb2Koku-}nc<4IFBZ_ELbQ3?MY^3O`T?pK+; z#6X29(xz$8WgTmaMy9d)KtLMN0>Kbh$c$1(b}$A@cv6l2d}`vRXu=V- z#|3>S=}d-bNaQ6(3<^X+L+uAR3-`n!v9=O6UlU%S!zI5ovK|qqtKHeQS=ZP1?zC7R z5gQeEwS9P9$8uwp$G$kzqP4o5SuHK*y3P}u;L`X#dp?NYa??v(D~Kn%i~SrPC)2N* zhEnYZV&b-dF%8Yulvj#~4{qf_dO|#7B%DYRKb#?2&j%|It8nu*Atdug) zJ>7G+$_xbI{C5?5TzAuRe+$aP+S+(F$FpT>+sili{dN&<-!8xZRi-QQO`!J!qEPus z!LhB3p9Po7j<)P<%o+%(v)NdEZO0@?xBX6g>6A5OD&=)}Ze(YBmU_c$N7Z^%oMH^35v% z!eo+uHjgLFe zPM9Q_+49_LwI!;pE7~&mwlBnO#=q5DzP-^3h5dXNA%Wz2Z_kGp6;2#9G^9Wu>*|s{ z#b(8wI2qRBj*}i=KOLBiG}Qh-9cIKmi$Smmn8S~@3&L+d_QR$TzU`Wa$5W`pnrC{J zd~PGZXWz<~bDQ@rr-jUar0fRQjgQa0w^TU0B3*MAx)xDx6R}>5@8G$R^8%^(m+c4= zk7A=%{@SrZ3!P_D?Z@7AOik|#ydMktt|UcqP^Y)@v}I*}G<}?WOUg@o=R1ecSxN zID37f2X6FF&jw3i(4iA?XJ52z{C4q?nGKK~3f=rI{N_qtL{HCp>S+}-(FxDqwZG;< zSAjIIxBHfU`jtOXI2>ibEoky#>{q`~3&=xb!Rp^@YuznpCO&3cogC9Ee!rW@A-%Sl zANi$g`95APEiuvTc>Y4xy%@5`{)cZD&bzSUH;>h{1sX}5+Kw@3lT}rNx5@7x>Ug%2 zwM&>lvcdanvjc#CQ85=Xg@3e)bMxjIFG~0t#5j?RY$abv(a5`YnV*A$9^g5bI@Oz- zdklyhL&0Ii|Gds8i)Y19&9=}IO=Mj#y2pwPcloTnq@$Fr2g3wn4Q!oY=WVBJQ@uN= zch%13SgCa9CMGoo;#YhrW5)%R(IY7choL4nJsI<|ytk23Z~>RXk(3l4fRpf_8v1(k z7W5R6-jxsHzCiW)9LtJG(&`VBylX8J9?iElWl=Oa*mZ0AfwsC8zMKA5nMg6svp1sC z&x|cAJ1&*@(MFjmSVD`(OoP<%<$9I8DDTOWY~p+E>{7YkusgSseQ%r{vb^Lo6)Qql zvY2Uri4a2p95XcR)VjWTeARW@p1q;LhwPT?bf>J_1(X~1`VYVWe-OH%Bx{eKcWYk^ zMj5m{|Mm)-Pv82A2EI$!wehD+>r8e9p@q6GJTyeceL`eMkhZF-6`UuS9MFAa|G{i8 zXGF+14Ps^f^n4y^ z{GKE0>FikD9+G?V5c1*ewRrB}ZQLj2Sn|yEV6X(3znqRo7^*ttcg`QH(43fJ#IZ-1 z5eG+`U)($0xGwPa;#(B}mJBse$~Ms&X_vqS-5rb^525GWmF z`Fry8>2lW7TqJ52w~svO;fqE)fNnc6dFK93uGG|QquG7*AtcR^`{kIO-HU+>guYrl zBlp#)*F>DcZpVKPS?h^gQ@x--!8Zoi57%6MGSWBstJSO3pUxjE9@PVXEuK8KuxlKk z)oX?AbUGjV@iOWPm9;+@!C>>zNpqcP5b7()2A&vg{a=r=|N z0l1sGrHyCA*(u2IVM6pGDk+=H8^~5w72c<-JNjvOPQ9Q{|3r*+{F9wkk5s0*3crc_ zDOgw>ckj+OatquTV5osfW6Ju<{_QC53Q&;9es(p75#}xX{?N^D?n_V;PTS7TwHX||qRjrnIr2fh)d!HE*b9njs^|Dp(OGD*~`foeC zdS$DDJyMW$IoGdRHEz_O5FH^OiS`|O>O^v(sUU`8#*kD9Lu|VkN3;?!L!e8+!5m$V zRZLhq!5hd3`gQ;0-dpB7Q@J(5?@;ItZ=olE^`NN4`jOrnp*vPD_j+v3 zVSvMLg6aY69YXbcPnT1i&CteX`mp9Qlv$#qIRK2O3T?=70s+2a_4$@jcBQE>!+E|f z3%OBQeyX`O14Nop>db` zxGezBdU;BLWJ%JJk90Du!2@GPIzy|7Y>k1TTKN@1Sfe#YooF0-SJxAE?K(|YOs>QP zd#~x=i3yK23{z>k<2mrDBKQ651-Gf|=F`SwB8fN0lclztk8hWMmit|ekf$|bRuDHd z!CNt0AAy_WVoQR$Ro|mIDfzQ6t}ti%xMSh58-61p4!*-Vxc=Z|WX>cgPPi zG4U+tvxVK`+Y}Lr+UW5nav#t z)s&)oGdrfXL-u;Y-KMK`PudwUxqKMQXJ>p=OW49snw*?`wPOO)6)M~UgBmGC(?|4g zkDj}odf&J}vLXF)cxQHoUHo}=qO=4|4e`4Pu(Xo?qMUu%Bc-a-36up7F9;N{t^4@O zuUQLS{bt*WM+4nSVeK0FwpMF@cS=au=5*}wC_RACf5@2SF=M85N+i%FH5+}kBfl%d z91J7zpR6oT&RXpch&4%wxZbyf`Tuq6UHV+*_;t+Du_sm9(mhl;2 zvC^ffPmq+%E3R}jhZwguI!~48*G=l_+YW=xp@O@@2wWciset5W27;uoo+49AJBT~V z$Llr@0@j1Z#?;(r@#oLiIC8Z6gD%CrgohC5P)jvG9m;J2E1$s&46}$UQTOkClcGQp zg*jo9ormUj|0?|8KqdcpGobsMMi79o;oV<8u3sped{2cpjRb!6(AphB{+F~YUzGJ6 z_iYn2NqkY>tBThM^r}&D?2zCgV}Z2!NU)u?){1p?Z+=c%gOF8D%d6*U(c{~c6kgLR zA1KTL?6f$MfkYtE2;~PgGyq9xf>J+tI6ZPaT?t9L3SM{F=4Fq4&mRXucC}nzzP@|< zW;+sW|12j@gep)&1@*Ym`Wi054|)f-oN*?%n|bEhPK?LnNl=CpvmvJ6vt5R$&1h!| zBzZ+7%Vz><0u+9ajj>V(umx&vce~!8uBv(h@PhUDgl^l;Lj9|!Yy$%?Dx}ofiE2qj z)FPXm;XG%m*)#x7fsGeZ9}Im+UcaWN--naLp@9rwMCgjuge(fnu*judMgU%2u-N+? zg3U3!z&jdu@*d9_a_t*Bo6#C}f#3V0ciAiEmFu=V30xm3{N$7a{BLwJ{VXYrN@lzE z_exXa@Z$Z^U3Xkv4FF8kM0?%1O~aKqfMs%H2Y>N?j9y5&rwL8_;8L4!xt0YOHoLvy zTJerCLu~(S6q;Jj&L_{KIPSyGc(GrvbX00y=W7R=CCNxjso&-k5G72P7CWAPA?`@T zWH>o>-16c(@1Kdk#}gR{fq*dd|DC814s$QDIE3pGq6x^jQ#8&=pvFGD@E9dylJ^F}!ERt?-noYN9j{ipGSS?009Dfn~pWl4Rh4qC&1yZAS+HE3M9W;6Ji zX*+3C(h;M4H*4aF=~epA`bRgszB2E%ka7euUP*meHmZ+WL{&b_2`5|RS+`QjWo8|pfv?WLMO-QOVFRwlIv)m^E)PXcP-&{Ne_39Uif!S zN`v=5oLW>T-(0zT!sewRD}~=F@=MO2`Bu&P}pok`q)JrWyPm5(?t6<@Vfi*EF_2hMgqH zggN3^Og)^^h)Uh%Kd>Y6v}GP3?meIou$Oqw)lw>?9Ot7yhI=SN`5gXOKy*?4xDMOi zBgCo*9@(5Zw>%=Yl~}%-Av;=0euORJigCXmqCrbb!$B(nu1%=xXkuh5|2k*z9Dm8lBOelZq2SN56(E^RHrU3^Y)SuzL=zB_Jn8p2!N|KL%P?^&VlxPbL!Qlq|)UxDj0RGCBDn z4cAUw_;QISbakwW6dRFx3=Cw`dL4OW>*Y($*f@7o8dEaZ?AhazXLfciT&)uN09r_Hw3Ul@{6-%>1&*Hr*d0_pL zUKya}PZLeA;39N3vM*$_SJikAbcwbJu-T_V)H+6>;~V)G0eRq3~$)qKSBb!asmb)Cf}=G z?`$JlV(*ZvIYMfLPbpM(^N@Y%q8dhUtIqeg3O85RJgN@)B$eKp9z+`rUqSCC6%~ur zyEl_>vfT$ z`f}e39>IpzGoLu|_3Des)#)u6wxH-BRtjN%8T6?Zci+SOb@jY$UZY}x$u3f%B~M8H z0XGhM@d}8VfvY2r0v5`SHTQYL#KjRPdWJ6J? zra$YNf7UIzBvy#=9pbvk?)=WZ|6Ne==>Fk~I|3xcp}53rg|H>(tPvcl!f_Z2rzZ@O zUxq9ngBAGs_Id1U9!bDpxhFHcs;+2TnTXU_!AK<{V#mnj?9^8=IB)-M=guO})iIN~ zTJ_D2)GDHiaSYjV!rb`>gwyGJt99s%k%5k3-Lf2no30@H|$rD7asek zLCN~_>5+VW9;O+Oj}WY5#`FuTG~9kVq85)vM@b>oZDAWFOR?YAp-DkUf+!p_M2GA8 z%yDd3^}lLT1LfqLTM}iY!MbCNsw;ABg?e%2&ePc4vlZL+5ypQUurJ>T zy=LEY^4uw%^dA6_?ryAY-4^s7i-QrBa)$#b29Od#`kCPQ{7i))r$o-f&GVeXARdUa zKgZJzSnd{_VYgKV00z7!$(TyM&tuo0I*5wdM}g zn0`%eZ{DJ|5;<0(J21cU)mHWDDHTes$VgYnn?%IP$xaaYwEgU}4u!+)*>ukB>G4%= zsd{`n-`}u%T)rKF-*1l28WIsF_x}e26oD?ve0(6Yz>2)1?Acm%pK!Dq4`wfL@;|IH zlc!Dfbjct_w1e&qzhHIsD!19-=UBRIJ6oQM#LnZAt4+7p07QT+gxnCh>ft}bFIrRh zn|HZfmh5~l@}+*XOC>{IBWzb%OT(?6`Ew**Yn>(ouzXSNlUY83CQ9Ff(Gz5-WZn05 zf1`Yid~z9@*@-^O_BqJMRe9I$g!(?EQ0zE~;{ z#dDdiL_G*fya2FcD1?N#Bt&*KW#g6Bwfpx&2osrS@H~8Ol!WAEbws zFyD^%fMOU?Nevzu0mS%MLmY8}$<@$zgQ-ws0i1SI!l9UB%r7*r6;>K%w}06oBfB}P z0yPEJ7to!ofsS*Q{lRa85Extkj~3{wWC>v&GjK$TZ}nJ9)!gQY{+)b%k<85DCy~k; zeV}%w>4SV~SDs`9a0kwhY&Cwi{9d24Pfix(H;T$G+gkKm(ns}A!>#1#<(FC)DgH&N2R(2bY#IluiFHz@btZW=3p@@MA zBh$5CzkVUQv8-ljh&>5zs!J$mxu<_4UQHNs7FhdIQ|FD`>W87xLBs`A17h7j-Vc1q z;||17yx{;&2F->kqYGi%FFk~X(=16yqsvXeS=A-)*0++W;X z7uV}W6#u)p7c(-=h1XhwdP>}` zCy8B@k8aB!TET;rw$Q6`a@_?Q0l?~G!b{pld4#Q^x2vH?517%h6 z`$y>R5Fb=YBM1@}Zf5ccp+NzuJkP)0tXtmF?*0`5*LKbSRrkS_yO>2()V8XFn@7X!&*Ek9` z4cbP0&=3>$*tMF^nrP5kowzAZ1&k0avAE3H!akMA655kzouQ5{j1I8GPDO5mG&#nzP1m+M%wqCBU)-n(8MzH65w_)d&z9azym5Nc{y9wUHzeH zF!8LYi1Ds`J?wTQPiaKz6=QYhN4U7+!I|P79a_wOFfg#f)^cXg9uGW5SpBW!&t^n! zcI0Z3n}#6K7zhHTN-Fh-v6pW6hhi4SV93bwcfj%dnfPXg)b#Wxpoj&clRkg`o)H`D z!6ClX0p||XgZR*t>@6D2gsvjHw-@k}2K6oxu`b6$*zoY6;7Y&dgNF8b^^JxJE7Q3ca3~ z{hQiO0Yh!8G}IwO2mpiw>BbR&23|HRE0g}PZGB^6(ul)N1ZSZujO{Lt1J$8nFkuSQ z!&u8%)En#1{N;b&4RrM|0Pa$|LdaLI3p52D{&$1AZf>>1KR-{5+D^QHlqILQM-3$wj|`rWAhc= zKtFhz@+zNsYd6-H(!Y7RP3Qq+=y-7i3>m8@sR(mJL{(F!$Ik43LceYQhafz=bW}Xd z)Jy6-IR97NTMhZCh_9B~@)ARG&Z&B_30~H?$nEdXv$IN?0?xt@_e-B#EzPs3(QIm` za1zOfSb+#|A}0?Xh~}*T(pp>F=Qowe3oq|K`5-0uSl%GA!WH%U1k%%x`&Ge}R8N-v zuaps*3h2CfcudXDI9K0Un2!~KFUXwjVdvqZ*Gs(c?}S0~GB(aMI#7KA;M7puSId*N z3eg#B? z@J)G6>{krCXze+gO03mm%hAgq zGjAF{*-V)3s_n?DLWDgOG@&zGzRr}GC4{8@U=i2A8>B~<7@3^3# zxjEaHPYSo^oI6WAQ-efE);7XVBP{#(L>wgY@Hz%4rEby3^1o`ihE$QfPX!0v6QMsK z*iZJ9Mt=Jy^ExArzB|;SCUGRIxTkWeyOHphH70)Y)RXsuTxXP0c2Nhs>*A9>H1;bs zLb0;YciflD@P;s1-}n^9O7p7wOhrY8ctNneo6e6EX*-P)U2-K%{|BD$@VDx7)MGrd z56MY7>?DXIApRifPqc*I`gkt}zCVWafRh6-8u8YAy~vsH=WqR}G1(C5R-Tb{`ja8Y zE149I+_W}Vzrdcx$`bv{Zf*7DZfX*^OKh6X6UIOSClZcD5lHC2g9iX~c=O%9a#@~U z+PKD>g{H)o6V+f8Z&PE_hM){031%se7;sHDOX;)4{90}G@^hasgUk%Cr3R{q9*oaG z)!`v3+uArX(^k+^)t0H*e^yoXFcdAm%^3M?FbQw%pby>p*hT%A7x5UT&L@d0$#d4} z87IQ7sR2u#gO^2(-^ZLrYf~3AH>t5}qN(E7uh(xo_F$lgAPL^tHwL=W z&5NhwB57UkSM5UW^&o{G92*Jr=Uo_si#%mBw9p;#k6Xd1!~?|1=|?n9_87;wZ(tj2 zjGKVLCSF2Bfiogf@kN)XzTx$eOo#4|NF;o8Iqg0QMxMgXR9AlkN+=@v17$&UIl&VL zp=8?o*2>?4BtT;`gGb6g;e*G=2$m5VpW4oXpR@1u_F%>VUzVRw1=pdAYvHSGcdUvg z&r0=i!NXdOZUNO`i*LYw?_g;LW&q{lNQqc7_y1~!Kodqp$Y1FTQlJJFFBjNjT!P@% zQq9ViM-^fQat0wDe`iB|pFkzK(RFiTEvI9yIDwqJm||zmhtb5&QKPdi5_=^QI2~f3 zETInN8V&FZd&o|Ezbaryy@89wqLhZv5@D)7R9CY7Bk9w#snk$IE2OnRnbi*<8(MPh zUr#aK`EdRBD4G_;0XG3>0(!FEXY=`4X}k-#Zs8OxDfErG=vE}s7F2UgTzKJKEE!HPD zbuVCN232-J}Jo&e6%q?}w)Glew@!wm%v~b8k5~e!W}y3>KM=YJE-fM0%BX^9pl#3a|;>B^Rtu zO6jOs0cTBgYJr{inTx1;W3=Xmjk@pKxm3+a98R6a=h_+?pCo$=gYZ7WU}ARl%l{S{ zaANzN^{}mV)yoVgVWi@Mp$B5sZ@tr?{8ib*Uljc+d+pS6hxE9!(d9wCxzi7mxq}KZ z49t(7JD-}Xz!tt7xyHuMFx?EWv7`|Uh3mv?%gScU@0t3aI?Qus;Ac84v+!K@`HL3; z{04th+Ev;+)qL!$m@v-=pcFUtvu?~r@k6o&T&c0xGYkZ8Xjbo`Jj2a()a7fqqKO&siY@2;22 z)BzI(jRSH}t-8Osx`(!Oh|5SXIc)d|l=9-7JbL5-u)9{Q5Kl)elOzodE9#MhgH?ij z^T_*6GAL$#zrt>HzvbF<*hTz!dZQ9^pzkC=mh^#f}G80og=6FoH z-Q5u_jkz73smH_4)8-Y%|I)7s_vcx;AZKk+5O?(ke_HV^r?W!jIc8e3W$|&CYVyy_-CQ@9^D^Tx7unxw6vNCZ0PYU(Len!-3BxWzgooaxzkjyDa}SXd z_Jvj(3k}7ON)nC+NLTnd={|MBC@JiJ5EjIRC3Q%kF?*pHGS@cQ3SVdvkg{{{s|VAs zs1+PM!7EmyUbOR2nqp=lhcHfkEK?0LYyH(9(Ca4P=0GgWRa^Z{1~J$dB%m1bY3Q#y zx4gOJ3o95~WZmP&*M%!11&+E15f7O{Ki}SKKN>o79_!Twc5;gSagtV&@uHM0j52dr{v8q91cA{?y$Y zfKE!(Lc6A;49JitCGPQ-T!R*vRtO+l@xKNdW>BfmH+k_0h_X^>7XR)k6@lk1O@*Qa z&ks}<*Z_As3(^H1rNBZAcpLxI^`4Rlu0)R~Vx?n^b0-57{4S@dIkg*!UD7S^xK2&) zPBH@jk=M=7W=}VLmcrSTy-OpCr+zhM`?&gHk(1q8(U>Z-lQhnbgBI1{bXAFo6}`X(e}9!z`mu>(qp zsRIVGX{Lh8oVXNimw!6Q$b4uXj1`sxwPqWYu!}F1IW6W&R3G1y&1jWlJ2CjS*wO3} z&fHtQ7b{LEd%y?`#M%bNyIA7<1{c%iw4EJqpWke~M;l!)*!-^eA3LZ9YyixmIALTD zIh9mUqjhr8>J2U3<|DF?*UXQiOi@cN$69qqRTYa=A_c^M#9SRIF8;Vi>fo2SyNMhg z-Z7#<_Ec=VSjI^tn0>YvGb7rr@DpRKP}bHyfn^bI);EB31VTs2=*Y;nGUgmLXe;w< z#BTllW9?-a%f%GHj@=LvV+IXwm z(@-wnY<&HJ92-l_MUxisCwb{_-XW@1+}DSAGhef-2TSg8Cz7~rWE}iIxy~g$PO1kx zo_?Cx+Z>cWa!hOq#yGeM0c>hh3LM&9O%w(p3n9Ut=-8c%rio>b)+UkPk5;rdhQ~%X zFK1}aHEOA?-_1xO{n;%okLID&)QfIPkrT;)mjE{?C#F)2tk3_JN^PL@JCm>t_^U~Jl%)>j0;qL;T9q7%l z>A{u{63H{=ZM|YDHH3A@{W{n2!Rbi(`tTAXV-}1PqL?U;k0b z666x#A0l~Td^wOz7P}M_#Tq)+k%|NUuU_X8nx%>H$%7YF{g*4qW$x%F^e<-nN)~kM zoISTPgQQWvuOmFn}Q$Siro(-?s z=m6mW7bjUf^b{93-rA@Km1O5BW`ZwC^eu}+`a(}>=II1_6u0H**@mQiod||3t(KV-$jz6 z@r%<8!Xee+h$WIr^qQa>3GmBHxaTj?gjJ#AjYs1^`3m>(oLuPM@$GJD6;Xq>@cHv0 z?E?>pmfsId|Gif+xPga*${HH77i$yM48_Qc0CY0nv{8@xWkXW1rgQO}NTn#!r;hyN zHdr#c?Dg%Mdw8blQpcZV2YYYvFpX5|tUCW8w+FzxK$fOw#POB^asNWr2WGwT*3_4W z>OS@c2GxK^ib)OCc6~{|d>4!>yimn%jS2yCaHkJAoTYFu56#F6;e?e4q(E^8A~P13UjMjbXs?3g7* z&%3%h$tT}vx)zR12MUPXO#hj8Q@B1ZG8WVy)h~dk-lXgenc2# zxX+sOMYW=3q_I)UBG8zDPsgMd?jvQHzd7YY;6(Dg*Pvz{{XBPu^~kvpslX1yi^{d4 z2+ogxy4Gbi2oWB6MHK%cr9>_UdJIjD1D{CnDG)Xm?QkabGRMUySO7XLD!uLG(4BgJ zV-e2;QXC(qr|+i^O2lU2chvd!B|-cW98ce=e8$0|4pn48&1Fg!yI`eAUB>tg0^t3h zIX;joW7)xXalL1H5{(DTdmaxZOs_A;FJ>DR`&*u@Op{q8#;^L;zEnM2@o0Zyu}%X@ zEkMvR25Ux4eSQ3{moq|*UV%WSk!5vjQ{u7WGs8dcF!x}RjS>k}&(KDjuGHk0#agO- zuLD2PPX%Z(mD)JUO;>~V;2}yO85T5ZGbQJ1~K6m7Oc=Kgys5o4zl2%XWcPe5loCZh=hS2B0v62s%qK=;@A zTVY+@#~qPgdn<(Jw{)g9ag^b>-1dD>E^kT#I%}ks3>g4?hSx5{TL3aZ6U!bv6YtX9 zG)U!$_9k`)ZBbrD!D;QgSt8!Iwyegxg0VKeKSbSRGostLc{0n@~?Q_8nULnWMbzz?ctw#fOaF!=Q^m6(s z;%=?Q0~0<}bwSN^e1ZiUP0ii!AwgM3j(2tUU9eLYrnLg#wL13qo)LD9X*|<77uA$E*+KfTM$hK4?x=N)gRIPrIkdYt|{#=1V~G zm*20F@P8uZm$Avn9iu-~)^M_~w^7_yd9IoMR&Nhwk$>v%^>xpXLRp%CQ^pSL%{Tq8 zg&8>S{yg~IrgA#8iE!6g1?I8N_pOB0g~w8N-6wy&?hPEVB=RL*++@%2*yFk1?I_W> zXRoqw4m~wVX^#;3zP}`I-5r9MSsMyBoU`ci^*PR+zwv&>zML0$R3YLjoUVjK*=duY zB3$ub#s_0@_o?aGaocN3#@G{q2sQt%vsMJ8&lzz5NDDW2Vp^;1`Lu`^nq!L9BB(8# z9SpCxgO(82CUiAW?QacLGlb%usz$?g>*D?+t6!%H^NnI7MxovIXZGLo6ibI#;L4Rx z6}RJXw?IlxL}ew$0WVd}yp4u^P+Qv#Y^SsQXJ;(Jpf`~(0moV)CIGVN`p^p;w6wAh zQdQ)#y0W3*AdYr0^a5Lnt)1hQG7$H+jgl^Y*1`9Co(Bs~9uLTGkZ18g3q zo1Y`@R%oAtHX8{0JlnW9!;LIMw1f}EDh35Lg5?NBW*^2`kKq0M!>R4$^LmxeZ2+bRw?|9YSZ+=0 zcYy=ZK2K~O2+(aA1$_Cy4C2x96H$Tpr~uy}94<%;xvqFYZrO${)9T)A~9sF3`q$U(*M2*9@MofKdRT_AQLD z0t^GQh!8T*j@_4t`;iXK|Bsxcg{-48ChXaV_#-dW6SkZQgAfE+!LoPmL{Q)(yfClc z@aacw6CB;lc&%?7e^TtH?|Xw|kws{x1u^pVF**KF?#nXg2h6c?)JJn6zbHJq*vr#T zPR@3Zaa};<*|f_vLCQIA{kKR zTQ0TwJ7v8}*2^l<@wrY0H%~azx$NuSZ4>#Y6Le5Xh>tg%IzwvE-sOkbBWocnp2#V8AKX(9m*Z&w5P#xC7)kU{K`0YY^46;NTi0QA4v|TKap# zIgLk`4db^;o6Uq6VekwRhzNy**u7v#AX48obzUn|XEb5nChs6D0q^JXk`7y#R#d^= zKG0^+4xa?#0w7+iF#nckr+c}Q4Fzo05ST#)J=#@xAgjF0Rn1b0L8Bp3*Kp;E^XY~z zFM=eJ8PJ0E0%l#@`87vmiIAx^^l=dptl*bCZ7g56@}NphD$N!`gqeP0NMw<$1`sMr zu|kip_U=B;;{wL3g!~(wd*OQVrpd^^783VulFIC)a=Unn#)InQ8f#%NAs^v1o!{kM zMsfEp3n$lE=pF0oqwQ5ob2r>m;r5Z*qJS z=>1a}f6KA(gmHoz(BValTc}y+|B+4v92GNK;U$6DEVZ=saIjKgK4wb~*A&B4;@QOM zynU1c(JW1glSm~H%(JTBX=}@VE{r>n$h`RYB4IyJsB}S7@Mw2Kd#iF|&dP{3zap+e zn6(fC@xf#B;>&8iF6c5yFZ~ybEPM6v=MOC&%G*c97qIPqwQYs(lh*63=;iI15`pX#6?%X;4`7=Ai0NA`2Er*eB4HiozRh4Q ziI+9VuG-MyrfPr0j%~jueJd7bnxionQT+kg{`wzmam59Z5nSXjVw+Y*{UJun!~Y)b zOf&I>xNj+sXHQ>b9yvqY+4TQ}6uvmW1$Qk4o*&-v@mQglhK0XA3Elw69{4Wbv2(jf zJbhX8|GgV$J>pS9GXL9!vn{)~V!l~cosWJb!(`(m;;Eextl)Pv;0EfeijADtw6%U_ z^VmzCIDj-y?D~RhBTh;r@lMpk13|n_Bq1TI6gxS{d(8>^<*7;JodxfoIRVCdr)kyw zajR+zn?SiaG14SSu?7UdpdSR<`ee;r>%Y&*qCZb^B)~WCO1TfQ{4MZONpZeN`NSa%hXYg?%y88 zcj15KGXPw0p`$s_Np&FM5he9MpNkOIWueFTD$;IlHNF!c0H>$v<%0qVy zmVjY;BxPWy0OVmMuK{t5xoJ~%YRk1DYT6VHYEK>N?nesYb51ewd)NXsWY+s-``>r= z)VF0iL`BzIoa zWCt{4SKd8je^5^c`tRYlSLl3Gk9K*&@A=1u`L;jhZ4%J;AY#Npz7ch(ryo6e@)I+p zZBye180R6MB?q&A41?ZMgF$nuip@g>veOCLJIdW4rb#d%-wTNw7X$aLPHDpmt1>gpib)`e^}rc0fpL&;#K!^z6Gsg9-Ey z{+lx*2@AU0a{3608bg>83jdJ6NLHAy4V(<{;|TbVl2 zEb7FlxRz_I>fiO1V%1L{+oQzxJqcb1*jfNbVp2ipdhd~EX`{^PTCu-=iJoGT1Qf}U z&CAdo-|PG)GR{nu^RPsB!z1~Bb}fh^@Hb<4Mf+$HK!zA> z?Lm)=U*%iA&Ecz8ub$bjKpnu?*r-kX&FQ*c`tZ-saakj$B<9Z1Y8nDAC5pG~yZPO{ zySu-yqfjE(Z{%0H78w=^%(D*_UXU(NwDJ4y)Up4o&m$s6hMJP0r;8znRZlbL4Sw@) z^m_f~4I+YtJ>MJociVrQ2;LvWk!GXQh=fbDQq*9~$zdGHZ@w^b0DvJ{13rORzC$Vo z4izFT@O#&A?6kO9nC8e3oywZfRHreM+T~P#oU||;fF<7e)Kne3hVTchPz@)zmjz$s zY=o?Q>H5#NL9ww)e)2~I%a%`};~By>6Y78$coKIMs@?)b&e;N|Fa32!BP0t3UEmPE zi3b?AvhQs=Ie&hyUw5A8A>Y}BV(L0fJS`l|^>Jy}PL8e2Ja8BYGFcY^ygEP1VUsp= zYA*sZ-9<0Zc&HcA`cjEs z{4%TZ!bWJC9PS`50p6|S$8+9Vs~+#&{3|OgDq8(HSP~w8X7M>85Z+t!zA_g$Q(^~s z9w|k5R!4VaiyQ2@m?qppbygbnBOm);C2(yv;&m}t8B;HJ(k3$%_(Qy{UdjG2DEc$k$z$W({k+kSZB|^yyOuVJiOt z91-S_^>(A;Ze?_b*cxp{yAX-KcBcQo&p2h0Lc_zqmxOBT=p=u8;khv(_U5LFY1Vp5 zuJek(OXz3{o;2~3Uh&!fbCT-Bi36XI{H3ewaVifqPc6G5iKK+B?kRVnq-{cxbu+NY zW2J&_@!IuxPX`4J_K4ae3}xX8Y$4yC%Cd}OG`o4sqcp69tnUx^%<_k%B8gGIWPE5B zv0lR=ImFqI-IjwRyu!umqDWlhNhw5OA{lAnCO`2eIXM7AkZ=6Df&H%YA@LSOn_idw z@mzhA>4rmv+diyKxjT*~IH04z=d60ORlFK`()v(ZK($b($fc( z^~HGTvIVliFbS&x$U>WcDwM}aO{y*K$)9-5q}Tb22l*k`AxyD$1;s*sF3)ZF3Q<~v z(D|d`Q2_Jm&%(SrT0He($2QQydT5ki_Vn-P#kYv~Qd2^(%G6m;Pw^wm72X&bwwi7f zlrkjPPkBx(-CADEE^jX1=oA}?&nTQY%SzUF=I3=Ku>OL zNJGYO-&^0M79K}k7maJ5Js5!4psX8%?}#YLh5yI!tPqxgF!1v|HUGoacgJJB_y0>N z5tXdak|eVtql{9hNJjQ7*(+r4N=}3_vqO>{vR9JCWh7aVJwmdw*YA0C?)&~e9>2ft z^Eh|s9InsjdcR+<=X7y(UHO)>X;G6|LW#@)7*nvrsD5LMHYaHx7qIIw71{Py;S6s` z-Pf*D`MCv!YV>YHT)S;FppFA%*A-Tvopql`e>^{44(#vxb#8Q;@4dPf9bFhvM}CF% zSi@+|p@U;hhlWGq*YEY#vdOfw0ZWaFT2Bn6B@-N)y#joRvZTlsVr`I82+~qk4&L|E z!57j<;aDRkrt4T$ANL}|7lsGyFB&iZ8!9;!i@@JF@WP1Gt9webx4mF4_|fm*yMgK>H)wIn z{6sX}O5IIrlnAcGZ;90WaJ1HYPD1r{wg-Of2;B*&r}o1!Xp|Qy$6jiYfSIk3sOy4j z2EfP2h!oLtvNE@Whll^eGr|aQQH7x$nitAA1E;kqYxpUNgRkCIwjE|>QTrZo^Q!T+ zwHdD0db&k_{aUPNG;H)}l%<+WDy8*V-7?3iQ^=~>D65^EU@|pPuchQG`;2JhprhpY zS-}=2@-|uss*yZ@S?EZCJ;>R4KkWW`Q!YMd@6gj>@-5dPlkI~8{9HdfHRX5z z!{K*@bvNXpqj*oCIt1)qa(>spApqbh=-%ml6`X)dB-m$E|K4r2NDe;?_Rr*viMQhO zFI2(m4x43c;Zq15XyM3r?{yv%1Zji~9yx#OiPJ+$_)fw2dLpbIC5kH}e4a2+tTV$$ zCHTHOJIWB(^+PodN9MR4GV&!YU*>d*Q>h8kc)poKYl@L|W&c|=rYE0fr z-GTfu*txDKBB$6O?^?IEj!yFzh3Vi)@*O>)q1@TtM9Oj{mu*vog^OW}1U61A3V?NW zt?x=rI`KR!eavgT*+-~CfF*TFiTwQM<9naUvA;f4E3@3;x#7AwYAwEW-8znV)I8vz z{+lUE=0i7=u-E}W9i9vR)cqs#VQbzzNuMvm#Odq_M@aXCWch6`KjL9 zC0!##ZeyfIDE{2bR@1-SdH!~9Mwk!b*tO+vg$j=-vFNuS{=6GKYEh6IAWkU((QH|~ zqaUAavyGBRt0*WdBvlGTyvt2!8~G$!F#imDC-`CBaa6 zl`W@V`#hMn+}x{H>zV0xwA?g*J*IRitw33B>%R|ZK3TjQxV=lckuF<12E=yNDy&#| zKXPmg7DNX!zR=*~kiFR~Y~{#^O9XPh-0k~cCMD4aos{&fj0P!lU6wN1MnzALwNP!> z!m53Jel3hw@fby z7k?YBoa*TsHH7cKh$C*jGq67l?Y9 zZq8D8;fiP+do&&LB^>G=_xs!{t?smNfP8%_cv|Z>Qw|d;YR^9!1M|ZHQSxR zap!%JHd}2~8>U7%b}*BB@Qal%^;WA@uAb1;8oNMk_egTvby@$he0RRbY@0X!rfxUd zeXmfve)(ZYYf2JkDu$vE($gu-fLj)^bk;M!@|tHK9$DufzaJHQ!SUYK##X{d zPY_MIZDl1{uZc4b1x2M4=H?|u#T*}KW1+GttaP6|=NwCP@^Lh7YK`A8MzGQKfwV8= zZ#n#+c9Wz{R&AF&-UPY^K~vavA zTyS?rZmvlj`J$TVg6ZG9rM!6ehvDIww^YvIVHP>flGok!B6h&-c*uk(Fu3L(m*tJI zQ0MjhDOEq?gM4e_ovRxyh7t16P9lgJIqmaOVnwf(bu$mYuX6TCVf6zZY*SoZN3|>l4-`b(7Vs)S20x(UwFX`0c zjXDi|6A~*B%mGi$hufxE`cMY=jU6+}W7V~047<+pSxlIJ`^*IEdy>hH?4*=2R!d9f zIdO48K{@+{4~7>Z#-ykLgN=3~Qtvn1bCeG*a)w>nr{9rf1e*K7(YNkvI|C271|GU# z6;)Vqi?as1l4Zqg03Soy-r&>X*VuwzzWSKUt>}3MVhe1Y_282N13%Q;=$e;l5;bv^ z(bm1qjY89&$wE>vW+mwVCsr1W5K$F^*9K%LF-&$a@{T`p9$>>mt}}&$_K3mlUHZ58 z_k(xAJDt)hsS%mk-!b*;{{AOyZ{ECB)Uk}9ge|Z$H_19jou=@@9{4D6F#|wCO$2J3 zyaAHnMAtceeid`wGc<;ISe4lv>iR`aFZ)lk`|ly}FI#n!x`iPiYxYSABUYfIBGtLM zx$zIz-BQ`>8BpW0pQH>T)q8UE>Vp8yOe0&R^ShvI#ZhL5p#s&KfjO$TXi@wqCmJ_6 zSd$}SCGl@AnNWpI1^ID5R#+qOmZP!t+c#lY5{;^ue?v%q?pzII2vFejIO$DHu;lI= zpKlcNNaXkm@d_r%uS(&Cmz+-l2}O|b>*jn`04Tcmx`;6X<)sI`kR<;3{l!h;5{~Qh zwZYd?U$C;s?<4<6)cpLojYaV8+3~V~JfXgIX~^Dk9u!tq^wu>B6>k5`F9>VC=!HB; zSdwK8xcqq5HWYDq)YObVpt#OWUw4opfIa+v0B;T2Tn-0`EfUBI*IHyeKS5mYEmDa< z3}CwWp-1|w2tcl`LJWts2{;(C1wh(EuAThrQ@$ouF2@r5l&8+?JcW=-kMZ#D?MGwf zXTQqcoZFb*5+e$wuerOsU+RuWz1HuzXVGufWY@Qk6P`|(UBIF6eE)P>#=K)d&l}$` z?E`;-q0uDm8$=rxAfor!c&mMrHIyO9HPV=$2}c|rdu)?}j%8(<>cGKW{bBqh5rHHp zX-@I#h5H?Zz>v>~xMBzlOJ#T9Cp?E#;Jky_9|3`26VN=n8qSr7X^0qLl=GtQ?zI$t zB295#XJC7W-3hupe@aY6-?tA9 z4Xta^jOUXShzjjUXiEcO2{!f=uV7=>^rpwPSCzKh4~~)}g)e^eYw|Xn@1A4H5r?ad z_-*GK3z0Ej9_>frTGie6L+l!N_yD=f{EST5_LYHkxZllM=G+bDw4p@rL{W6*VB; zp;Bes<8w$BY}06CK5-gHk`&x24_I9QJ3);E*awB44=-IX&wAz3QXI_B8pqbuxY;#H zb>InGYPy{ga$YPNBzXQ9GFXZ`oaf0UIRxcrxLKxZK-EjhWTgaE$~SG4TN!O;vnyq^ z49x*!;tSu1G$rj}ezMb%e_(iUfb$o8IHYU%t|_V*)*@W{j|uT3BU-A6+ZFvE+pMnO z;13_$x@H&eIT@1bBf|qpdQhV2IF`pUTlah_tlSV|eJ*_TrBYRQuY=ISf+c#D#ZI=H z7e>D(lgMM4Y((C|cJ+H}s0CQr=-WD)qwW_Nh&796`*vm62P_1K5~)TJ6^1Nypi^{o zg3$SNyh>FwTWH6&xO)fc%&j;Z$i4iw|E;vKSNQJ;L%2vQ73Z22a!);KSHAJh`E#zB zu&%NNBdmy3LMuN2L+4e90MQ6rhQ^SdN5*Q{>Ab>Pf(S$^p;gV$54l>>g zY4n>5a1v&TICLk^dfnD;1L3gHuWWm^gHSU%qeBH04vQ=-qPp0(t|ouA`0@Q{vqE$f z1=gf+S`xcS3f}{WcEl%2aL8@2+KT152Nn%+3Wh_x0-M20e zJF3L?S4$%wd_qu@<&L(2Ei$9Lu=?w#!pmPU1_xot0l))~3$Eu6sGpD(m!WAFK#Swf zojtfJ$0zOn4NXQy^XL=T;`oTOLnDvMh?4g&sj#5THJjx>TN=ZPhw3I4bT4W|hPh7v zD9&&;K-SrjS?m=NZa*)3qOai44pI+~o^u$&cwP(2y{2O4ID{UR|Yu{d5m?3G^U%eBhed_NhAw z8gqudE_Drd5|uJEY*A^{)uBLTjR^frtXAouKF3RwuOmG>V_*KLcH&q<;dW0$&6uao!a) zIm!qMgdQbFB|9XntmLN!>7NSv zboXp^Q|t}9QIhtK2=3y;Vb`TVq5{hSxkEtgB-ORNDta2dOjXlO4fYU2+p|}oO2(^i z?KU)zClMdh`ks@Vlv&UsDKRk|6ifU{q34<>A+X0UQCKWOKB|LZ&nNO7Y(j}`W=ErA zpLC55zRUA8E^PjX7g9=mqjo+FM;90yinfeMIm1r#44MwdpIG|Q&JQ$8ms*Xb8wdxM8$nD#d{WKR=eTtqo6k$EE!q^U=OfYR>-Zai&~b z>U*0p1#$^;V2pYIRnafTfV5cDm%*rz>)}oZauW_-+|r29J&nlyf!QmFeD=8HjWbZL zRCIytg6O65*=4<-bECZ;RxyI(zoPhYTmav46lK>lPxA1F$9Qn9*j}cU!n9_VPm$+5 zAgG6E3xMj=L4!-ORIQ-3vV|lSpWF1V#mK#e>za#61iNyc9ILHwG)&F_F>6>+g+c$#x+&8?2Xp=8>}COJEB?K^+^=dDRB*Li$nyKX++vLEdrs3JPPQ4 zYO2lY^Al(tL(P5mcw;WoY92FxCkCHGvgJ#?@YY=IfM#aNOA#-T2BUIrN$!&r-1+g+ zUnbAV(q}_E^u;8f$!T@`@+~#gv1}PBD=9txp)1EWSYyQB^n}pl(AP*eZR8;o-Q#eA z#+2sC+jZAV7ah7x1%jWYsLD!a)gE|0Hfbtg)}HZPGuf`?yJVm#gp^1=dQc^d+l7l} z%NFQE@8%-U`ubQcWY(yXJn#HtvYq~21%UGwlflbJR`;Ri2U(ST6`hyiy8o&)S=QVw z3227cm<2JUqcym+7boM(E~0q>=kCqsSE50=2?=P8J|GkL2boj-UB(xX+H@v6$+CF1 zkA+@Vo`gGNw>qs9OlKH*fq=6F5)4HMPZsE}W5)2m5w58QP0q!m^#G=@RKfN+Yh(KQ z=dZ|!|AaLoPh3sRAwdC{H}E4!+Ych3vS@0>@i~5K`IHJHYGUik{{?EjR3K4Wzh(Q< zvQy!Pa_firdew(ma7n_#)FfOv#|B}ppMO;016zzyo+*87qhPd!AhD1DkC7Nvx1Sek z_L>?>dzYUSvVUJ}2e1&gu+BN<-3o8lHG@5X^ z9Mcz0iL)Sf5iU@r?^d-u5EdH7BS9f5z*gfaWbuZ#)7?~Xa{RY=O`O;ogN_v;d~$jG z=9Vw+(>SS!=&7PslSgGX-vNIT)hfi`#&1}!_V1XBD@03e!ZAL4KNM+AO;725s|q9D zX*PFAD$$xp42&yD-+T(3kQj`I@b4pUofvhBFL)5%SJ(uJCeX&HgU|Ecd`6eS6p;Y$d=bBWS4me&YVeq@pd zRZ|q)EL*VwK&TQE$zeH$+11Z4QG@rQX*bQ2Z|{~P5()~OG;WNgUqWacJ_g)-ek8&H z9DZyeFoinub(*FrDq75g`1MZ770F6-%Q0k~xTI_jL%K9Yx1C#uT6I!;E#~csD zHwT1f@A$9f@AK~rNl35Q7(>5nPfjL(HWP>QA1G*ttWEDI zBg_5Pu5n2i^sM`s{<;_+Bhl_`q^@d)O)`|g#Q6ZOclenw!a^UKEd*E48>Q4Y8QQwE z>M5<@V7kMnNWz#*7`9HvX=Yx^RzrW*@u>=2NRISUY9oa zEtKDEd>iwpFEU0|=+FnBGw^B<4W|>NGZ4wcWKa3tASgC$>>lqGV#n1Z5n?F$&!2BD zWvW6Qk@M2O2?+qulRO}S^}PRHL%_`Mp02A}T4ZSb@}K_M;vsTgY%L!qwjxX>h(^J0 z3K{rG?~9?3L?7KJMMS81ZYtF4aIJfsI`4{q)2_V^cE5Yblaj22>< z7us&AUC1h4-o;OL)JKrkT)+(4b0p!yEP>q^Q-qb{3y5VQ_*+Yfwi!j9|w}5*HV56q#w9U*u8Hh=C0a#k^@g@ zAq@u>1bq}e8)>rAK^#%r-W&1X8@_r3l3nB(bXq8}%|Q=|8<_`G#4S!!c5T#`A8lkR z3&vtXRBVDhu+ENAE_!aKHDa|-j~5>>+|P=7(^qJ*@^i>=QxZp8=_EMpkNRh?;4Q^P zCAW49mr+ z$rpCX3rjagbT+S(dUGIear;|$*UMFTv=P`TiLeA<0~mCmOHp3lpri~%xywOAC`n*q z#A+H?qgRaPt0in7IO0b)GSH3ezBGkw5zG!q<3rqRbLk-?eWDT_MJ~*&wgLeobCG-CScRnkQ+&}M+YJ{AeVMCtm&mtrdc4E9-c{o_G^2swK zF%D}yprUFrtbL^x-9_3THbyXie&tR@sVWe^O9K=C$@U`iAwwe*v9a*VeyDOI4^)<_ z0(!Y~rjDmjv&k2tJVn~EMv`9`$KU{ix4K`hq6?HIK;09EBkts&UI;=dVAcrZzyb5* zyQWO9d)=Yt&CO!cmq<(%?~w&2lF{(#^$mNw!_AFagc?3Qc`$tV;=nsHFaYREPLB5X zj)pLMNKT$#wY@Yj32E5Z@eFPX?p>GmEe|qmMImG@yMLictRN(W7jQ)77>pu}up)O& z>OMVZ&WX_;1O907iz|x5cy4%O4Gn{5BI@qqq}oS)a`?>z@z6$f4>#q9;=vc@-jMVx zc3*%$kjDSSN?W%4sF-STZm5rVioHEG$XNV-h@=?llVRPBo{oqm!<^6eF$zJ0l*uhn zIU~9BfG~kr~kK_cf12)u4M2cZ$v1({{>%VTERQhC0d~ zYLQPmxc(ADK5df4iNvr54vyhVxDpZNkBAx2zetEL=gywAZ4C9ypf48DRg?BImXSTj zbk550pC`LhUnOQ)-5!8tm|(Yo`8)FIKS!^2YbzV@y@@>V5?eCrsv-Km@MDo0dN)~-thP5zh0SdOKBBk++tU}FPhj zv#V80rg@W9kZr=;jZ7w_E%Q7gD1mio=6+K%Re`VPL+r1ayLNieEJlu;@<*! zG1PX%)J9ZPKK-hcF8bcn_N=f@i9S+ZpFsA(_!Qaq?4=FFfS=E-ErlowM0KDABfQo3jA0fc-f6b-~7%s3S zn7h9Al>LdIuy!UGfi*Z9O$YW|PVQrDRa1|lpA5J4F=D><7;+KNPUyvO53mzLNEtmS>{qoUR>t|vy>0lo`c;-0JV^O#!Nvv|1gpjfkbEbb8E`%PgU6Wv^P21ZlvFJ3$F z_L-KwK-6%gF|Q&rM`N3sp2S7Voo5MLu`CyndChS>5E^l>W(L?>{QtK9@Rr6H<-a&Z z0&S2ei~O^zSJ$$YuI2RK*Nh7LdGAHm#C!qMEmiu2R#QZ%U}2=Ry}gw+n-k<7gB1CG zyZAMl2S>m3^$~#K-`?EltMsp#tN-+lh^FHyv%dd#K2jr&56d>D?VXs(`MLwFvhL~U z#~w*|CP4YSe&dDMRyK@gu;2i9j>}~CV@1jW{LD_9D^{>f)FAm)U;?5Yj5dTDRRlUD zGCn;gFwE+#{HWRn$Kal;$>fyhA}VO0%)EmRCdPM=FTu15+ZZu@q)A@w()_PmoOH>_ zg2l7Wg2l_f?(jB@1Rrp)&AJH(wN7k^g&caRDf_16voP;>nS3zUwQ)G4wM1!wR41-4x3# z(;q`mGFA(C8G!8Q#169^X3|_1+teBP*^+cir44|xP?Ep?p#Yg+uHIu zmn@!th{Vm1_5IPB6NjkzyE6Xi${0dz+!r&IzjlHLCz-|1kl{n>T!3 zYuS>aqFYl%?UyYPqeFhkHe%-wa3E5C>i3e`26;DqM@PRJ7P?}>DeWtdzIgQUQS$ZG zjuhieEjW+~mgT@2h(*m_-@a&Tn5HBrgAj)kGx$fSfBwS6eI_JyJRs4vrUjc)^%C~U zUv!SinQTNkCL%3q6d#3$D*=aZF}a1{71VS9mVZ#y$>nmR?u_!zLEfL|=gfdC;ogU* z3ppIn9U$WeYY_|~?(QM5_uHXJiO3Pc#tH<(2I(iT#Wp+rnCum{jdT#JV^wvr852D*nFpzQc z_fvVa_1|7WTVRtmb7MT54+ngadCe-@9ftH@&Got?%K5;(B!BZ_yl5#Sj5MQ;Kf!-@9)H3ez&(D_!M-O`l_~etQ_LzO7 zH`|4qe?Kmx4rVmmDr-jd?RMvZr~2#oQx>}T75>Yd_9N4C5)R4!jv_x7*xkhWl9TN! zNv*rj)MrNu)&Gz`7$(_KC#8n$ zJN@@!<%iIWprj}bAk6AZLe~Z8{pRX z6X*fH#2JPH#qV9&_%6V^dtRn4y>U46qP3kT2 z94nhk(`Fwp1W5ZJIKza9nc?+;k?h^~$av$eQyU(?rwgn$7~?np=*=SH;C54q=f>nN zGy?aQ4O$Wb2!&_=-FnFM;cze;0rGAXeIv zQp76o!3K<%g_y|#Hw{z%FX$Zt*i$pOas4i*6*dp;PH4SG`m#nzeWflvVFz2`AvB;N zgr^fRnfaQ2G`;sOGXp#Z^)dV<;V@28T?IB8splAtbmSNk94cmf znrEQ-G*O8G3ivKi+msNdeTo#Eg}y+R_71Sm_$Y0J8(I^)cA#mxxabm8uQ^vA$g+n> zWvW8#e=1rFmfC(_&n>xIr$cM-p3Upb zBZZKCPJ8Q)`LI3Pca`)}FbfTh$8MN8r!63w$US@q>Wp=KK z;^(=jL8CiCwV!ZN-gmqSeFZ@Ge7En&VD>y$$yr>|#JGG_-Gw{xc(&&}tX(@aTCd77S$YghbM0j0T*jeIc z=%u#mdg^oZba6RtMD>O3E*rnzS|uFK{$s6$w^NUC3d zb{Z-Qu&S8fj9q!egR(Dt%z9{Sqxz-$bXdzv^;0ij0_Bx^x`5$TJr$I9G z5(io%yeu}$vSw%RkSTJ6a_}91FC1HE-SMD5gG;r}%L7(c<@xSSX(a}=fjQ-^Bc*!Y}Lo;TiJV)1FU?*&5J zKdoa&!UexD1!(4WiQ-3wct}Znstr2gxw(kxA#**~GSSO+S(jN3QDImoXA1a`a5;0N zKpAcVwTd~L-u2PKmT%ukX{##`axGCerULg6%G+%oRDFDULa4QHq1Q&{BvG-6&;-sT&*PcN;5$y@7%S0q-L*w`8K)a+!%2kZI8FE zj@4xSW zJN#f7^E? zsj@FfKIPP|FLsJ5`J#?((?O_yyk?Mu>{_rQ!Vaa*ajIRBmU&$6F`V^113IVP5k}_z zEHQM~I{t>h=u7GM52g7;-T(CF?`f;W+jzM4GNc!eJ=)i7RvSol`aBcPPP?&KAqyva zLZ><-hw5+!nTpD8A3v=NC)rs!{2sKLUQ+avB{4JO{4$v@x~R^lkZ~(5TW`5UMP=mA z_`uxb-tkKv*JFjxV9CT3KHoEDH3WcOGJy@YN`zRv>vzt6P4WKgr_qZ?#V5^iA8T1# zj@6x9V`QeZ8@|d=*!x}lcz;i&r-a1DxCE@X&9U~I$|-S!_?-REi8Luu&L|nVHSXD# z!t3KDOPM0u{b+JBcBDI%gTi%5a4ua>jMzYrAZ6R)9n&rydIovLDEWx>T=v!D7Z5-t zP=(K-3lotq{sH-Ku(ntd*7wtX*p{x4srHWP>YBUgaz9zzQkf-&*EqM_c*#p>(vVI| zH*WglHvC*h#A#+GgP=_7<419!vGH)3?ppG}C-b{5lsXE9##e9O?ty)kr%z41Z;;OG zgl6Vr_q8aD4p>uBhfvx7yVP@Uq3fuTb*y}Cg?saDLimrel}69c?n~~wLrjN0v4w^j zNq?V`o?GEcAD`Rta5wu&|7=%!e<)wK9<({CS#?qEJkYvH7q=u#D|IQQRbOCY%p@{a ze8_5Z0&|4MjB7?3r?PU5p5F_()q|IE_eqlH(9n=j9DC%XdM%|jM#A%O>5ssw{=wzh zZ6v?T?AC%un=_N;*nMGjvuNitecdKW^JI^K!Ld)Z=s2xO6ED}56^==)?5nklFAWF} zztaZtbVuD?dPs-PMryo^vetj^c`Adyr6p(8hZsnUiwsh>;-NtKJv$F&W`@N;abM#v zyO$*UF~!9s#h^Ab5EH?K5#z>t-^|Xjod5O-v+JHc%g7z!h5A8OU0ypiQeGC)ourufs@1t4&y9A` zR+n;zYuTsL(qJ)SfByO2@Mpfx{K*21fxi3HzWzT-;& z%z6z3U5Bw4>-LWBk5_i6_f3I^iFsrc z^!;h{nHA2sq|dIMG_|toM*{JY_Fs$)TBK(qMjm-}S;RqaMVs8YdZAmz=c5&*0u=5;SX6B(75q?R%_@E!9%;7e&Lh|e(sFo|*SfoDe@+LY- z#ue$Yxvq}KwF(*zg;oyIW%mQU< zQKnHG4zg_N2*pug!f}x!^gSM1r@`LY;D|E&mPt$NKBC3(n(lm5N8_7{=tIMez2ec0 z^qcI#x($Xf2FOFKw-hF^6xDHe z1q~2Fh@bqy>>=tSnKLJy-qvuCiRZp}rNk$k;qiQ$)b}wTKE3y8jQp6>@&NCga~QMLo9O)|;hgc; zwS*Z;Z~k#P6e$-c$6h#^cOZf=l6zKw_UOJBTSatINYCGiZ$HM@xt~ep_yGB-sI2av zKLi4|58~_HJCXL!`)4)Gx<8hngb>*pE1{O2U$^!3H}@&1sDP-byE`W!GUNZ!kQLq+9mq^NXKV7Y5F;^5D` ze(l#xz(3esHkPN(z23LA=IOMxwiR4DUEw=1p$XYCI;2;ZhfhjWWEgxg5dinajN}`# zwDdz*TTiB@af)x!n{PFaQT|z4`={osFiXwLLWM8fr~1`seeN^ibu!ne-1o&KRF;2w zk6l4SleYYsJa<*h+rmG`8Y1@>SZ5nfwi9lTFFU<>{(PTRVL(&U#2lYsczW(v57X3} zwAmeji5xH@ano17ZtUV`ypNE|gG>}>&(cJxXmCB+C&}%*!%LFJ2q@t2;ofAh;ionw~(h}Yg6XiEJU-P#6hJZj3ebK8ygMz%g$R|(El=XIH zz77c)x~1x(Nb6G|$OkK@&FmoU+TuAK6$16#%HX;%vbvOSE?~^98==6BSh4nF2s5!z zbLks&hX2MW`j99KPD!5>=5y`x?<}qMd8_|NBrN1q$>6IEbb_O{(L_ZQ`FeHCi1$Kl`E*k z4y&jT2ol7{Af|IZ*3g0*^^Gr_M5$=Git#s5f--2U0W0NU|K$2Hd;&P=1enlfXNJGF zC}3o3YEvDZulYH>bo=yA*i>(~4G={?m}qk}1A1J{U7Zak?&Frl^C6l}ajMdniI!Kq z+wC;Z;LW(_yoKO_785@h1jP$h-o0}oRJF9;-21$47@=rJI@_~GxVJ?T+>Ys^f(G`% z0O^FhJeO;Q16%9yeM6|roBGw6^NRuW#vmMVdd*$%Av{4-@chqvWONL+V85AOFrmMM;AV4{mELys=so~!yhYnI$d5fz|2#_F zxF6pkL9gn8qqG!FEBV^IG6`>U749a3z?yYp3aUNNb@cNXB|cDWhhDyiCtjx*L&F)+ zZE08==mJjoF8-&-YBJIKYD(djgX7I-1UeJ*bZ#~45!;$>cSACL_K+bEKj?}|)3cG) zshhdyg`7^_M%l?2A=*El`3y4U zL%685BMioxTyR3u%g%1zWczB@tA@8EF7BFbvFME(RyYFrbuUvN`VLcIb7F_knKKPp zI$9mScXAJYd~#rYL~rvP%=;2^$l5?s@YBG@7~2gX5Brs&8#0sqDz)xmBCTE_@+%_c zta^P|nG@da+2KbSi6iZwu;9PTqEV#BT^NB^0#prsayqL66JC7OR1A(^ELT@jF3sLCoYqdxxB*Wlb)qn6S>oyd7 z-4IHqM%NNI@#eiAGjtw*lD7H*fHbGGGIIXN(m!1=Jbz0?gg8k+y9zrO!Ytn=(*H;X z1YJ}@sAM7iz~B7z0L6)W#oXV%aZlUKMRlyxqo~7r2!R-txO)14Lasl>4zl-NhYsHh zdv_HTYK+M4;Br`oCGtazRE zIf9rz-Sl)Z`egc`%E@`u*xY<1hS_kddCQqUNOVrjwsp zPptfWcwt%C+V8bvd`?x?vx|k5{%M?hvt8OIfvTK56HOKIp)z0RDU=rAdV}T1u$Y_V zQ{X$UtY>^ZJK*6%Z>rs4KKmrK&=>oXT|a-;gDtMy~(6JHl57rE}J?vWuc{3uj*GW!#ro) z!v5O4H)Uk(i$7S41LY|x8QfEhX=+sLN3F!Bjqd8#G1Ob=p*@ZWMDk@!2zN-HQqZVp zdC%4CSbHcIw9Dhiq@tTn(*!#2g`UnHu+t&U#n;1|1fN zZs#t72ZDKvk682Y)XZQLFC1tqvBTGuc(Zb3rQ0>7xn-o;&O}!h?~?MyLEyAL>p$rs zS3~uqreG80GJ8Fj1%L$HQZc;4`k`N+BvxM5)m6p$_;MI0C+G5v?^YnH-wx~`&5QR; zcovVp4uSR2Gsh~AGsShObnEejX*V)}iy(6;?w=PE>T`QhxlY(Slg#HH{z-bR#Ihzc z+YxHFgNLXF?x}w^njfJbZd8a`imDq1cWeFK2B%_C!L@&Jyo8*b=Xf*{jLegx2jZ7c z<4vnAS{W^0pRC;Ex3}jNJ0MVxe&E1>t#AST~Nv-l?b z@d0pK-6USwa`u z^U-~SAAAl&ymwiV7QDH(YW_eN#?jeY*C$tkF$*tFMZ4}C3VU6+t=U*#-wQTGXOgn@ z5k6uYweGA<7G1e6)I#j^vLW((*1QS$%FGN9F{e+p$HDvLjpk9QIC@9MNULVXar7{`791bH!>~E#tx6XT^16LOObN^G%``mayE~RA)Fhw zbDwnB0A7G^g3p8N!Z8=q+i=pgXG%Jrx^DS?V}toZo+_Z4h0Fg!HB1hru@aj@5`$RT zh$UgQ&YSb`zKI-wXMR{ER`mK*-$oh)rbz&4+1F}Xnmrczb^j#x#mtIisuIF^ES<_V ziPziGZ)wm#cD%LfsmOS`5pY1tdvURg?!v=|?&~wY@ptuK#>+_)5Xp9gSU}2UX&I-o zH_8(OHoiCL(6K7bw5z*)=M)JG%by6X(Cl{xwbU?fTE9%=j9=>+!+F`QW}jg`V#_=; zmqwCMRJAZOAdjoT!}F={k>BiL^;umxnGU-@-@kl$+npSNurP=7=hGD1?&y~W?^8fK znWOaqlA97@qSj8|dwyL?QegZY{1C3g}ER^v=i;?Kmls2r*Zt;@MC1B)==S z_EcAU+g~WS#i^C`I8QSGZv+#w*z$&rMZqA4ce2%I;0ps=8xnmqmrPp+jh&08!%wbkjm z3u!8crZtPp$nTEZk)-f5E%bP}?$XT;xF=lq{J4(!qO>PZxYxy;wEOo?gRV9pgkOg| zrLi$i#m5W3v!-U*$OylkSO-Q1DyrXPM;Ing(O#b!4%v85XXJqIUdOL3 ziZ{fwAts1bcko|UT5(f+r+LG9ZMy1V=DU0qqGAGvfK*#qag1MwR^#H-)X#;Ttj`zp z9wnr4CMY~eO=XO1SPx2e6R-TO9GefW5QjV7 z-6Xh@{^7!yi;3ly3LV$n`X-}+!R@c(VJdgahfg@pKRn{*dv|VbPikD86@GTXXv<7$ z*T-1Lq?6(lI4&8E{F+>D%l4@8*K-q+9%`WfB<8x<@}qp2{mTmQ^ux}tm{oj$--VZ7 zK_A`MPIb4C3wINvERQ_U@Ddg zyq{V3D<@x_G`N}J=LDzK)YPvGUbPsVgQQt^14pTZzTL@T#Pr0ErEgv!Ab|J&c^{u_r-a# zeUPOFk8>zUZ+NXw0tJ+|E>aPZaz-uV0wXKoljpW)D{$aIA-Yav4v!PtY0}cZByK+K4Kh_Gl?#X$ER_u&^5*hYu#-uvn7&2xe2frM*hMXF1z%M25JJ^8AGp_t^pK z@vlM27-dAd@D2|wHs5ZAYFjz0+}ioj8za;P(XK`Cl~~t>EU9u7#L`aG9Qd}6bheJO zfTp9RAOqIgIhEJ#?Ca}mYHAugzwT0gTgsYueuZ?eM32;YG|hC$8XGs}@@?lW+{X8g zeAw(&E8Q3gJ58iY{xsUNs1KPXrPf{M41cYQNVg)TfaEm>80oABxWlaUQdT3Tn! zH9j+Pp!t8oIA*gxHF0@is-`~HQ-zpz*-OzdP3B0NzXl4gk z+<9FuYJW*+fhLwuv87*0i61bZ4wv?cRlMex-uBlTb{O|ht@56A*{NQMmAX^x?BRvN ztnqx$>V{88Q(*`dU^2YHMPgSh8WOS}w;U`jhmogkWWoJ3Ee#8JXJK~0W#2_2j?{GC z`ZNgolOB!M>z`!Xe>u^A{WgryG7PiO>BD{gnkNbKJ6sZ2T{L&-dI(wDI#*n~NK^lDUhE4tPH|LoiGqcbib&YLeJ!l{pogPq)~Q7!UQH$J>RrR?-$92aPwx9?=)NXYO!hTAF3BYK?y?mPrV$zk z9T=)3+6r9E1Sr;)gj#xar39nLezwG^lnCqs9kUayjN8i}^qT1CyB z;a=Ch`e(=S1(^?tkI~Je$kNWb$R3iCn(EZocVC%V!HqYJnFM=#dC=9v?OGRLF$qLC2u3WuXL9lg4P2K1 zji}fZd!eSIV$m6do5b>;6bWzDQgLRlY#82C)48nr+f4A@hUwqqPbcvwT->?y71jSS zqmta-@+ryiSKYWl22Ml&bk*mp&#thRX9%@RC+&~5GOb=#13`=IE#TW{#1Dh zUri;1W=0Vgml7klO{^R&u1mf3fUyZ?E0g1q$n~MYI|vdiy!*0RVnG4p94*Dfxh3|c zIo4-wD!(qBdDoqvj(DL792Hn=rqk%JVRrYCyxmo$%o~xCo-SOSo0n%)YIN52;e83BG7rsdd8Ig zP}SX>bH~3JqBvyf=PyTw>pKZ6=w8n?ls#c-88$*yP}rliXM1zZ!GYyj?-P1Zv+Yrn z)p{r^>mA;`GY0Dw69qAIAMjGO59QDfQXAZOxU`6fK)6=HmoANm-IlRAJFY3xZmwo; z`9UBZq@-*hnc_cNP+0zi&uu_r)=89bKCmq<-3}8jLB$JBr5p4U8NbwH@GbfDNwTAd z?<($Ji>w9LU99*EZD0H+fOvkqgIx(NU^VR*3cE{&d7>vz#Pi)lD>w-%)>X)&Eq`-X zyH&i`aoji@ztZ42U~8JA&i(O@BDRM<{iWaCSPY5zP9S^mK~Y+rTwF6iVdLKSnS}+- z8a72N7}~<=iP|wJ5x)6pJrTNWL;dmrWEh0Q=P#IlnY1TbkV0-QhSUucRDuZ!EhY+W zqG@To>F4tb9T9dXW02%}txqdGpOJKNX${Ln%hy4jIaWl+RBvuHivCgfyY()7H;yfQ z{izB7!;>fE_~cNu8f;Ba!W9hc4WFkrFXb-kcMUG<#%D$DpNEsbUg?}bk#mc`!`#r3 z_|epqRh#oXzq;~r8%r<~3(~W*z^gwBU)jjo;CpW)%yU;*PenzGfs9$1P&;A_h~TO8 zMq-3}G3i^UzKDwH7L&A|JEDDOYK9KtR>lOT{%ekg8KaP4a^||2&V<1it3vD5#3Pxi zmz6`Jp?rlMWAA3ofM(N1Wck@#ey|L&2;!mla%@z|)Qdn<6j33~Uw zz@zHhAK-|-K&#FLE{QEf15n9GJy)sw?|WZs z0$OXw{Bzu0r($m3!EG?$TgpLu3nGx0Sxx!ug-A!sWg*d(1*3u(VQ47MMxNTmk>2)w z0^;1cZ^|1)ygO1ZFjLDsoNoAr-j3XFzHgct49z(G8jZO5j5Yk)9(lpw7Z_+(Qo=6m zYzw%e>+FyN=YOF=z%Ds<#v8#w=4aBCv|W`ym;}?z4=BhM#h-~ zWS7{q5Gu8|sh0%++_>lDK&EeFaWRc$HLQuUW;(e;YtM$<>#!esYLwX> zP5<)h_W2L>PkXnTYN+JTLxJV`b1D{eo{Xz&TZ#1*dJz2X&*=4Wq1^EiV4?zLv+BY? zWRe>L74^C}i$nv4rBJervf?V^%UuwcN$5ByJU zE3wJ+y_e@Fi}#Y3pft4t!a+Q8f2o%qeNFq1oZJ!w1`*NP06&RALhEfp?H8$XxS}CM9Uoy9y;Ah2|;6ye5Jpo ze>;ge0r!==V^qqEd%iMwHH-lb?aWD{R`J2yVak3oLcH%xJl_`#p&)R8g$w^oQs8KK z(*P=W`z}oqMkb2T>M9h(Rd=eJv`iM|l72zfbemayHW&7>qx(|Q&0}PaOpPE}3SX61 zN+MW~wF=wsO=eDk(!O%};ab$*t(UZ-3`d&l?(S4k=~x%3xJ5@tkFRBpy339903v?U z_wReJdM&#HKQ#c)?E%tgH^4=uyL5u=1m2#_b;dMJGln)Af4aUm%^jrL6ld<2ta#Fb zX~!T~3+)VGROrow0asp0Wc)0)@#iQ83;?}_Jzpzct8lq;g084z6zOo)OJSl z3=e$TX=yCPnI8-szLTkzwU6N|qJJBdNYU@;?jF@= zkjBxKCda^t>U^J2Nkqd3)Ed+b!GL zHB~eYJZLmx3#%u7iDZHtP%ghODd`yg2*5I{qJ+S1t4V|gp;9C^TAb##Sm%@xp2G91 z6mU8NKAaC|l5GQ;x4w5zMjy8vaY|zNcS<^kIUCg9fUO2Fk$?3d;nf8TSdS5GGYCQ; z%YlSjbK467UHbd*uNA5qrU-;pEz@NNE}9ZIF$NWEoU;)mzSV~u?`N$7i4&sS9!I$m0s4Xs~*jZV5&FrN!pUfrONrj=c>4)HWm4#{xb~5wYPBRMX$6R{&K-bj>?r2k z^mP81m}uJs8}1qkn)Uq;&UgtHElz*8N~vQl9)Z~vV|%|%=){5^8jY5Fro&L-RmXpu zSLb|#o;^3#Fa2^BpcpX!IcZyvviQ*h>xx6BmSG(QGJqNP+O_&yY}YuS_oBg2AO}L^ zPn;ab$gD)tQIu<_9$hijE-kt4weMZ@9lo}28y{ckZPS$S?eg=^Wj{0LvgLreHc(0L(0(k^(p{Jl!{C6| z+<@GW{q8xJv`bZDhF@8O_{2H3Ip@3J)?=b?+}@phxvs0Jn)E>tGeptE?tBXDph9rL z(TC4=?%a-h=f`x}phTm3z_kR}QK*ny-|PDsT?K#G^!oc{+m@g-+*r)#YWcW)?`Ypd zIp5^>0GGehbC6gLRwbs|Sq%840AEjUlf}Xg8?liui&KNS zX3XjE?lL<-e@Vl-h#dr4`o!A3)vRjiUg(7m0isD1a7=kclG(9srmLcXscI(DDpFq> zR>*&2;?Ta!bOM-rZ{1{n)MnC)i3vGOX~5feKO(KA_nLqm&nQ}y2&j|={`YWXm`E4LRF!Q>kTZ~s&0LU~%fVXEDH zIV-iNR_MHoIA4T%>+&1&nR5Cc7iaDNyjz|0BHS0P6!FtwXok9>oX@dJ<@rK;UVz^B z+^!xK=K}7B=32N1oeQOo2DQJf8EX_2a+Ig{h>kAyc8g|*?6~dpD)6m)9|#+P_bH!* zY6FxeF0sq5Q%pW_=#%yM=}YhC7Sc{Buax}V)mwJfSLJ9~jKAGFi(aWJLX&-GY&Lh+yX#tF4+CrLJ7mrN&DY@oqTCe~dCkmlK_CnU07Ca8ZZ6S> zU~i;AM=L1!*lJVe|#C+T9+kwaf=nFuy6AXhBGhS}|dyq7w zKpw<2`T3TzSdc#H=d`gp#pW`LjKXed7;)3nw?TYz+*-++a*CI9#B`q|Tx(FsiaOan zklLr^+RYKb>^gkMjHk%Xc~t#s&6|%${pk3^4C>sD*3kRO3`@K=*p2L&ay{;qhFixI zNq#bWY;73;t;om>x&5L>C$t9Ig?%y+!g8cfgBj0ic7YK(F351i)7lzOnlT{2siT_z*;yLGHty7 z7)TR@qL6R>B6=C%F=PHy4~0ALz2d|OTsRd`MSXf#A}RR|=SKG_k7so)DH+ z8gG7ODDzQbsN&>TlklQI|--^aCMg?s*+35-%u7IzrpQ! zw|MnbD$v7Nd}FNp_2r&YS6;2~&`e7HVD9W?N*g=uVm?+F8MKp<%Nt9x`~ZkJI&v6@ zr=qI>RDcHh{{7?kw~@)BE{-n%9{?n~ze!!$S-mBOl7H{jM&#acTuUaI7+OquEB`9* zK{oorm| zALm~xlBhaRmYyWhiI2;QigRsH;b{CtXg}e$36$Q~h>!>L@1F+B7EEzclM;X7E^;nZ znPZd!y8J0{?UANZf<3|3Xj4;5TIK(BJ-kKrSK_S^>+`an69nT)bEZ3J|Ds726U^?w zr6APeXR^VRGn_%WGr#N8-E}p%)xO-cg4X(+a*O?(V{pugO>c{O?#R`=SES#N({YA3 zxaYa>U`mP>pk$O!#2D@W*r(gn{P)m4;198lqAV=l)J8&8R%fIjF-_H+CgCaYA7sLo zoE!XxU|DNgFS+!66hXI-djbr0s?ptYwK@`w=T4-6D{#IcL^nJIN=Z+h#!k7{ztOwN z{t4{;!wc*ckox2ABGH$28lC`I_c|rz*;!8_b}6vA`O+`D#^jbqD6o(LfzC!mEzw7D zaa9BDfu3|TiptGnJ{7T1nPM65HnWG-m{)$tPa=bB!XzXXap>unqqoy*)D-FC#IIdT zBzThQHHmjW7ogWS2??t`5#k_0L!aRY;NE12K2qgqX2;Z}pdBPFGm6NQpHRiE0DE~; zf?~ScB<6NC8qMsnks!{8G`=a5Vr;S)S=H|BY^bn0yzle4Aap)2d zgaSzTni+G=uuqQ@l!y?ZCN;^M>2=DMZ5y}Z9yqgK$E9Gjp?QD?5I~@0T9c8X*^DCR zo56h;Gzmcpj<0Y}r;vg1s1=GCH2R??7iZ>P;hU~EVn7yJ6;JjdLV9${e7CnEjey(u z{Qw%;`Kl&;9v&^C>0Vm!xv_92jat6*N>{zY*5Y)%bvSKWOFz$&@cm4DJ=+B^HGFD? zSNq{n%G_s4NzvdF4k*w_yA-xQRK5@tS$hBevS?B{9^mG+o-)bz6(=+Tn6mK3UyRFu z-ub9#vT8S$ph;4(2Qcq-eNRm$V~+9)4fQ5>hU)%6)5QbT8(I<~(GM?FUvd8G z?gBvWr1l`Ln*h5AaSvECUed z?2L0E6#0W82A`3kw)gexSA##{w)x_l8v&q&$*(F&U)_BY`Y&ZSf|=SUW41j{D&>Bn)gC$lZyrl+lJt( zOLB734yn&OOb_i+w_k2HQgdJ9cd%77F1=nek~v!qy~oUq5z;3<0T&l%?QKn8KIFMM zC8bRMP1;y{=sLqpW-P@R3^5HW`<>m;{!^MvMuU#F2A3=jO!AvN>r*^}>rb#u6OSh` zk%-a{!8SoT>`*c9yW={{gWTqo3wQpBV%dZIbKM_QSFjl~$DQPUIN@TdI{0r&tN2v-H# zdh{yzH|~0;wt;hF3H2uTI4Y?vqvj!%(_?^CvHvYxd&Vs`uv`M>IrZRVsJ&a&w2kM)Yaer{CfQ1VTK< z4MQRkRksk5s|2wu;AtW$_&<1B2{;Xz7)Nk$1rv(fIm<-dl`!~V9t^)y^?Hv_nJ&tO zUOE#rtssG*?F_RMr#|8V$CLz|Fz?<~@44RGtTg`H-0g9DyMFih!ICeTO zbZEayOAEu6^nY&>L!EBaTBV@1g54&q?!0*9XZ_>2&_$f=p2O*Wg!>pFXto1{La6tq z;M+05uK;gE>6m|DX=EU-a(EipfG%MoACK2TxeRkq_23GloY_OlQ7etnvfsS5$S@R6+sfp9oH?t#zxg)mE>|Fr zvXhqICA_CnZY!OE?pVX1{W3=H@SX0tu)%ShTn4xJ3eq4zlfm2d;c$R+9k&d_$AN~3@CBBRQ z+QIHiA2dLUkg(B=KSY)moFpbd`Of!Cz4tYzB6F@cWixlAq}kyt<%vOc9%N!SAnYzI z%-Au!urTb(!$4^64-uOC&6a^ItIeEcK9KG@lN&Iv;8UQH0fdFma`52kXBE2AwYsJT zjRvIGjiQ7!%~@*@K$@Y6O2 z8aO!mFc5+Fp;r2-btIw|FB-;|8$zQY;l7hGY!Pbp1BnS0au`x@#fKa}D3xBoxbNu~ z6bMPOtE`5@T%2iH-&;)WmrYXw&YX^uZ=K~TR@`j`lqj9`Jb=L`%!G{n6PYuj7KvbY z;RL}X`C-2&lJp=lw->)=cF4Je+DylYE2DQXSj`>r-v6%#ouy?l`zU6l`>>({=!d#E z(lOCLJNps=bar$%E76e7%U}g-o9Yx@no^voI}{)v`@V-BjZsO3qKTjBa1@^oKRx$F zc+`mo3%EJzJdK+c4>nwfN7YP-)R=@AiXr04`Bjgtf@*GFh-aKpK6l3ewFhO14rLUdcO4_Qax06U*e|=|-js53 zk>on}qde;pVU~nvac;gP>U)3L5d>=)fWyk+Gu|8Zij!63Yc@N@Ekt4!(wYj*HC?3fiB8B@XshgDTVWE0i&$Pu;*lyY2vd<>; zR$yIfhO}YydOx#_b4COKZJ{zQK7%(m# z4mdJ7CyqX1Ra(9k((;9Srlq4bI$zWE&YuGI1WzZ@2XO}x9#1GOfogvKT-(?*&CbZ{ z%obc#31N~W79PD$a@~Hn!&J}YsO-LNJ>w+)RjO4aQO%^y2B>6Af)!Is=db<2-0&Ip zM|dBu#`iEu-lQ=lF+=YdGte*0Lu%>; z#u-mahPjn&0u$ZF3Y{1cn9+!q>_Ox0BRSK&WOa20kbDp?v}MM8w{n16CgIzbPBR`X zD2ckkuaPSMcj54CjHKh?G5d118^_3%2cB#I$_KgO_bHFdK7rM{XXe$Ehx!!>991sEA{$;r+pwrE7hM2$0Yt_XtKPV@G_sZvWBb~ ze1*~ADRysI`3!5c?4~`8xs}-31A#RsRp!U{BX>K0y)?5kyfZk$;6dIXQ*m=h@iHG) z%?KZ%H~H0O-54IfG$brphmD3#Of-Su_kR3Jex2Q%+dnD+w{>(hY+*B#uQqXRya#zc z<~eDP5LBtVQmoL#sfXQ>zI=9Q5?*o;5R>;9JKf8#Y4A=Fj4UjeB(0f@0vns_e51$e zknjlav0f%K@(K`nl7?Ti(|JnMIW{tnIfELzw zox0nk_Q72E*rFs@+d>I8q>4Q^I7zr6?E~?+WY$EOHZkE0g}$m4v36a|-iWi@#4fxQ^dXvxKmCh;C&(#n{zJsSXBsm8*fegJ_77-`Ox%73&JTy>+=MJi6=4g-5_$FrT5l zIhv!XEmy6-4m0fhymO~*OM&w*JvOv>7QMy42g_{{FLqus&;RvnHM_BH^xOz?Rx|So zPvWwZ01Xty_Yg$4$x#}q)&8w-y8TV^JLIpTo5f=(c8|hrTB9EuSC*eVs&)Z4&$dU= zOJ5eT*E?OlJRy3Fzn2goCNw~4 zzJKlR6@C-2Ny&^0V_WId74Kn8@r&-x7o zAV2|VAz^hA=5bJ{g`RU_c7SnW?zKgJ@J#zPHJV>jWds?|N22uRry%N%l(o?x?z}khyrGw!o^LqqCt) zx5?J~Sbm0C9OT~NlG@4N)y$Ep0`Op$&)uM1bR;#?gx~pZf6OJV2?fxCoka#3WHCUY zgl#WYYoN$r*xhEKQ+(ArGoF=FNvWNWXXFv4S9o#}m>GuT58zXc26F zxgPH4zKK?TCSFNK&@tX0ea8vF5uPt-F~0V4)eWf|H$treDb56-$6@HcEI|+TtSoN< z75jNosmFtfrp&ZoQ*+m=`rc=c4~Ow|GsZbYG)5iXSUSl6mY(LyN{&{Vi;~9!2u&cc zEHP=u7Ttu@)Yd1XriK=#qJII?~KmuqzfKUf^W%9RJKz zmpyNHvlXjJu4E(g9DSd1{OO~oH6PLOlF&T>98gMv;oL7#BP8|pYpN)7%CtMnm9c$& z#J-=yF#ntNKIQSRh%MI@KtSi0!O7!iVzZvt48g=D1(8!hT!>zr|K%+ticT|bP_9;g z4e9W(Kra9^G^^Cp*-!1^uAWUV+(b*e5nTYdc+dyOM3{68IptFyV%mWFXx!2CeOI=_ z*-R55$%7xLJa*8~0(USpo$j|tt~9cb+(C6n&c8$4$EFL~Y9VxV9OMQbPNGS~E1dke{a3S6r8dwna=_*FuAwk|BUJV*dURf92`1j|YN(0%AcvpI;g_6gt(%22A>Q`n!>oQg zb>_Xb5K}dq?ovtD<&!+P`97XW)om|3k!tZ_(FpNH3p$9i2t_DFhZ?ql=cYW|Aq9r< zv`+kZExzoVnW9u1rWJ1;#y5aw1N|{$irGMB z0K#;0!iKuOYoIOAi~=LVwd7MbU8O(uzRNDWmdnwxwxPRFue(uVyjXZH zzi7eQSBBfwjV30Y>4aQ-B3FoPA?xN{D>n@mPwu!_YrVCsc|n)yO;k?kxDj;>WB)`e zcr+6B)x9coT^4<7OF(|7|I;IJfCeKkA zRy4QsmoJ~(TUT9^>f+Kh>s;FDFudou1ZeP!?8gSB-u|s0^B?$EUKM|heo!&6w_4|` zj!KwN(d*Y7NDnorWrcHaU1s6(>_AaJwge=+X?>BM*92h$4c(>|Q|97t*V7}KVCVkI zfBcE6*#;disCY%|jwn&iF9{GMF@ip{C!KTstZr>qNzRttL3N0?%Wf;a0a%AnP1g*F z)kATohT-OxPky{-&sG6J1{8<-W+kus_l1&*+GZ=7?RrGfq}H-nC%H1Gy4U4hus7B5 zlXl7SO&|Dw=+Lk-&-8eW`##dNj9clfjw)V=IXx)=e*gnAk z&5-vz zbv#j^UPVGLV8GmQF;zV<5l~*znj82N$K-+@GL66o!^W;;t2(v89U6w6e){&K>OF)9 zO!CLHklmQX1>WF+?-l1Q#ZmxJMupoth(+x3EEONvs+W*x;J8m^@dfge*45u0ZmHh9 z-iK`~w~@L^2cx32jKbc?6;6!;epj5n=~$V#GMyVftGoWmI`OTVuk0 zzJb!8Ml9knKI>{@h@-c8{SHhXoo2gHUZ@#AL#7JBW!E!a$uORc?jSgD-|o6>xuwD} z%j#;*SIQ@Iz+$CD!h2z2m~E@8#VcZH6uKbD5y1o8oF?2&KEvHa7?C+fP$cwIS1 z_n7L77fd&-tioG93<)lxOV}lq`|-q%izz?sZl9py2yiXo&!xXoo6KJQwBlwJ#SQVu zkbjFC@7t+O_x>{a=ofxoCAs$-C>KE9La_&~D$pbP2dMZZx1uPdK{PLk5dyr3{}{WA z6574M!AVeo+cgDpDVvcV>Yg;jg@<08vA~cIYkhN$i->%wkMV!m7-GLPD(POH`M1~7 zk$m+?^*vqw7qa>W8}1|ASt%?EQ01n3ac%eXGjEi15WOx+)Z+{6ABBtNcpg4{Tn`tm z+w$5Us0M2jV1wvLBKclx3LtwScbvXb-2ze zDmeZ85|~pOrVy-25aqZ%Ku;Q31t)Q}s3%aq&9HKymVu%GDIpaQV-tY~4!@1fL#-d` zj1(RZ+lr~ndU=sE->%>ZZV`^p<(u#xxBfP}5=9G$7X&MUt>8Aox2|%OyxghQXe5Xr z7Kfx5GMUHofi-yt0I>cEo4M(UOStf*EK-fmM40^^+AbvMfOTwWg$Z}nb{}cN5Xu{D zJaIg&Uko$eEyFJe2eWOtKKc$XQ`W(?a&{4O&8iYOLL?%IEFaDHJSMGW z=WEw?%E`$k6&8-bC%4OoV(i_kAZQX&X=T}YH{C~%qhsN?+fu;!eTH8v6!-Q6zb!;& zmwMb9L-#JoSm&p}=>s^EOy*XfwThdV;3py_8qdhpET$kK?4bs4PVbo)9{9jmz~~2{ z9}YC+AExhV{hiJ0IwUC1`Q|~XmsQf#-hRN!~vk!?WpQk$y&t7~2 z&`tI2Khbd`EYSH^dzJ?g4MC)>U|!+m?BQtcPX`?MVF_bwzp6h*zA%tqn2& zw!~TB1QU5JUE)brncwt{g+?Kf1FRzr(I4^>CKiW#GfJ6pJ9a=>_y*||e3P2g+C)aRelY0LRNU!84Inq5O5`fyxYf4^2WtsqGCMJs-L z(S6mVbmrkG<7A{s4{4dDDP%%Aq7+*RY(GdZO@)eb+`Di!p*UN{|z=6(!)4_ zmjH-j1^#yHMmbaNZmohBDF6GmK@1L&*Ox(W247H1FnOTghKD~{sk2|U_!5#hEBEVw zK|qKNl+jhUFu;Izsp=cicveBf$am!_Y)=~ppguDBVW$RF(8AcIROSD&;gYmn!T}LM z4it{fr(b=Czy76?a)*8x;1~jzKf^GSqCxI#Q>{U)3tsKp6fZ;Y;GTXNTJL@tYTAuw z1(L(A-rn2y%YEERu&-fwd(g*^A%PEIZ9DL^7lzKAc<3v1_oMCIB*z^15O%V6PmIx=8tKy@GD)uU!}=SR{7n3h58LokWf62=MIBbZEc(=KU zYNpQquo^TG<8!apQ%+2Fo?K_P-d|49NNJ4_7CR{ z3?PCg{f>EKvOD4$F%Dry^Of22^n&-$kY875;*oZ8Qs$)E)aWZY^cnl}iXO_P{0W$(>rODGQBuz{R@nH=o;!yO3+f07|3_ z{P)!V&-|cCZu*wrvy{W>?b$P*A{j9;7je>@o$2ocRy_N{cK`9A5_ihOR~oMCBwUW( z0F3hV#VKum`b*Dqo&>$9nO7~*NjIJPnM;RA9Vj#4nC1vjf6H}S0D}wgsK7u-2^r}g zU5f6xwCk=095RrBV6BlQo*iM=5ekdR%Ced`UbjD@wttG@Y>_aAki?du;-fjS$W4Z1u%@{&*aUJJ5L;kAktBkV!d-Xv!&9XU(oJ4a>rQ{# z%25vJA;|wKm>TwwfGbln4 zEG-k0ToGOdy#$cvD+r#r#wsZx1)vmzHxb+;dgl-(TZHX_!z2Xp7#Jzalp2U|C>AQ* z_;|eTHA*z^taB;Fx|*~-@*{VJowx5WUHI{*5Rw%q0~a2atv7_ydK4o@D0*jpbFZv` zmid%*acx9fXM4v#SU9UTr^)IFk&%G7&Z3;&+Zw!l(elsaIUj(9p0L$SfeH!2O)f6p zq8oUISG7eHO^!qsY`6}e2{Vn^r-!mR9hLa@g`4%GjC2>pPMUuQ7P+BlT0KpzXJ z5pip~5NLTAXhLg@J7j)YslwMDHgX9n7o0TQL%mi!1;>)#%^r->cA*~spOxc3ga&sg zEXov{J$DYyblc>;%gwQu1cEdLti(cpjc^ z=4dp7@@YNq*`izuIk8(Q$#|up0a>RHN-qLQ89Fa1CVlq)?i{BXTj-d;e4gCa5l9p z&8w@4=cq*O}9@u0r1ZE^Bg0c22AQ; znYik*)gMF6*bJOw5a|OL{%=_D%csV2Yevb$$gAEi2SR1nA#^Kj>hHF%07CF5gw%q5 zm>O&zWRC_e+QKoE7CgJ$tECm_327TL?w4H&uh?^#!lP6$h4u;)t;*ihVbV6#KO{tg|4yK*RS=@rsgAfPb&>VWLd62O^XpP z;z;@YccgxdPCZ53YTBLDyfZQ}_41tGgKX4_+``X#(x(2LPc;zJ6sWck(a+uvkeIpS zkq=K$G$Z_f$q+42wxBp|^lg`mHXF~K=_i68+7LF-ULN6nA!e!(F`?F96 zXC}Wq(e_7Mk8??OnQ<>I72ew5&d9f`gS>lp7n;ypJ=s}Wl<+2DSCgz<*=CRa<3j(M zzg_1SXC>t30^6et3VtSTUO~-1)zn0bYZ88I2<*VXLsYVtwmLDjTHhKuiCfjfAy$W% zqt)cIZBhf&FEFF+rW3b_1VI0<$n6;ffcygsvGs;9QiN<``%sM3XOj-4WS!ImOAK*9 zaq)J+#)Sx96QQCbff2eRfKrLEp=T^UM(;MY6G0&^{P$Fz{@3vB;?8dF_p?L@RT-b_ zp_RotHbU>jH{>#9_w!o}tAsPPTFM7zZEEjVIzqeQOSg}|b#_%ecuh-0F_E5T zy>1V0%AB*>osP+)nN=!^@ALPYSg&>gwp8hy8wiGA4+yz@SzPzrfnaqB=oGZlA}TAZ z!kjufk{tflYk}0HlV~k7&B?;y@km>7PGI9e?Iwsy=2b@)TS&n7L*#%iV5Glr+^OyAYU2e-T z_e0l-*+J7vyZn&`*`i$Ge?Jiik)INAIP!-cB0HWQ1b*&VHLVX);FuW8nfOMei+ah# z5auON+_Ew-Ci9Chy(}o0{lHbT(($D-yMgk_wBbrb>WNK%QuFP`8>1qJz}f(dgFGnC znFYcn$bE@8tMbQNK@T!O;@;3dhyX$vnR|mCJDy%pq%1a;LWG@;1=8yN0}9t@we%~% zdGS@CL>an0F|HjbCjR;)cV|4x@mT{T(XSMP4tmc60rB|H+v-(&%mMG zb09@C>_S=#_U7qVJ44lMBc@Ed9!dZJo9uE$96g zy(3kGd6!t~i-ddpepvQMAc;NCpIpGN{P_&tTo5epvXz2z=yDJ=(sA-4=X0o*nfLIR zZ4nCjd-GTslvduwe)3da@q8(&bl41Fi5RJ^n7Cm+BqaVIzVs0VJUIfU00Kk`s3UPS z0kB|s>}}KOh9*IU)SD4n3|y&N=u5CBN&hDMmp@%wch9%NM4K-9Jy44XB{|&HMPw^v znh4HX_AibeoP0;wDCE6VNJm+OxI}l?W4Qk-%?>`@KQPRA%qot)hYGp3p z{78yLcl>QH&fU?O zRL@C>d9~7Fa_+3QO`y-;B&xZPeEoea_9>KN=08w_+C7*I=5XaRs(2?{b0qGS-tj~`ZsJwF~2 ze*L-TI^_1iQqIgYzSe9}PD*AH?zqpe1Ay(=NbfnEW}_8W#~1^#HI!S0-4_whbMc1M zwE#~jl3+!^Y6fAK?$8%exS?NtJOijZq`LMEJ((Qn!|2nqg8qJz<|3I~antI!(Dq=7 z&$_qwsTKOBr=R{clc?IK?Ny=^n{LT{-e2N#ZG;mZ7n&`;O##L!BBgJU8Cq2laF(#Y zIsF?lG}SB`-ycvBm~+^vKnS0MTEZU>36MT9 zS6ST-u*P^Jpr>({Xu0Zsxc$RE*>ep``UKar8(a|LZ-!JWhoIQ02 zSdYfcw2$B51bu0))IOnsse=diLy^4KAH8|i=_ju^nfwqM$&J)o!m!a(sE=f8ETtAE zv{SHS;2&vf8ekQ-3$@nDpRP@h%FEvm2JzU~HTPPnwj5dvOU=r9h7$zAYrtVoym73A zo!s#f!#m7Pd1Aw_@MxQiVly*Y-2eR9i*n+IMCxUc^Zv_59!h0GItPKrVczewIJ*ZIPe4U1qqw$M#Oqfd^ZKauzyK2A zn5g%JHh8G|OXx#)^hDSxg<}hA3FzypJiov|TKGxx0ZI7J{_b&I=Qbtm25nxipZTI@ z{{&VnzCGh{529;i%XR*lrX^iwKrH8TwWBgx6W&YMMTir#_%}XYfy{uyasK>7)^A+$ zFPYc_wZ@<|sDD#U@j;1D{Tk9ECa0>MmKQ~+{Xalj1}GY!K6x^Zh3ZVk0}GK7WU>NA z#;^9PRl&33-E2@(EqW4IQes`iOGZ4wHK3TPzsX^Dxb$a#6*43^NFCxxE;w>T^r zbuE6c`uGX>`}+>@a=w|0)eYVGVJ#5+6 zKzSK>3m%-g*lA|$pw&TSkiceT5XpQvN}0GUo0UX|pWPeeB!34fk$l-HPGvWX-$p-< zDxhj|67%(Li0JU&iISLVmz+mgJ!sWu)yBm1BV=kcCg%PNSt>z6!KA!MK#wV`5}jTM zSF_+RhCJga-$7N#R!tmo5V$p?LiFK9UKDZEUlZa7h)qSNAI6~;h#3wIYbr|YYOh~N6m5AE{7cnX?gYs9(b1{$s z4Ry;WbB^ZF+{9~N+<94QD8t2R52z8^VuXx5E-Xy2V(87a)??pMR^AlELzbGD=HGT9 zx#fnBAYu+6S3oFOtcyB?5qfJawDHS)r+WxmGETblnWM!6=NdMMBj(zjxtXBi-|LrO z53eGVB_u4TFLDGRw@`Slc$37(KV43@jXOR<^>l@c>u9*RgEKieqGkx{@f-Y1#LClv z{6BeKW3K||$>dQVWk@TU@|6$|PqfFRVz#+Zi z!VZYHwU3vv`3NAi1j}OZMxr|3r?U%8 zsv1Im1~4gh7CFRK+%}$i)s(t?z7vw*Kw#AWUW9*Ojbx=P9rZtIP@mk)oRdykxdh+~Q^VJ9-#Nl+h`0y?*w9J%IyoY0DW2yX_V@R^ z^|x3smYzgQJKu7XHXd(y(tm&!*Y~LnMCOJ2?+)X7!(Fu3sXQn_j?}jdpH1S)rfzA; zZ{~Rf*aeRYVLL#Vng_)`RWhu5*4E=BeWR@JB5$yP%?ZRL(yTORB-$JVx5<8b*{^3-yppIX|&1D&-kB4!|1g0d}Gx2lRa}gR%ZR(sw7vYdZwc33Or9a znduMLE*WN3hI+cYx+N^#(Mw#>>6>#?ZZH!3@Hfs0HcKQuA@q9_bfz2u$n3a5LIwU| z!vdAfz(Aj^s9pLDkSNsEhLLdR9#nL(tjR1ZNP!*@4whpf=zzQ$b1k@U9Z9~xehk5p zICKImEh|O+@L@m?_A2MjJs9;hr>(Z9+i8v8V;uK%n?27evG{?xqzNZl5XQ0|wVA=e zsSPNPDE;s$x0@XO9P0l_6j7Jnknl8Au7rDE2sksDi{0aX}jA5Qu*NTg-h#f{; z8pN>TkaWgljb`rR-^Tlhrg?;oKLhFbzYik20fq%*mmj(j!1LDjpTs~8*yc&x^nMj5 zNkH9oJys1g5;Pl49R~h5gBm5`lP}Qc>usm(v7cu6{=QPt<4K1;dbKwzy>7zTA<>EOniHew;PJ?%5X7fkCjr|G6N?Yt5 zD89%jk$Bw{yb%f#=`;hsFg}nDzab!V$O5Ub)00{nO!%9lf$txF`*t0E3Zy)OwG*v+Mg4N-jzsG*rqz6lo{DQVdy*BTi>KanlcDlDpakhLK~uwQgXYUa zOAz#@eAu%`-Qiz;PZeVMvbGkbV27vpaB5M{RbVZ0XNr+!%t?<$AE_4w`*fAmC{z@$ zT`NHkYewpy=)Z-9Iu{`0dG+yLy$a2)LBZkn=z7i*f|`*;4eOs@-3_rhof3gLXQJ7G z{DrcVhsRgwPvH}a;1AdO=`^Rf24`$JLDUI^mY}ZXL^ldZrnOW-Z|?R5uN|$}$&RwhF}QOyPD0LrcgVhbDqDe*UOhIZo*sCE2rGx|Yh4d(w<7fTIUMCj zn|uD67H52tCR|)=cTm;;D4!#|6E8+<93k_?>wMs;?uGZ(2B&sa&6`bY0Fa1=@#l$m z_^4N6B76(@=F#YY=+$Ry2*rMta|bemci@|he$w;cAeWJ$1@z+~pKWqDY@Ra4quU5#nj@ zTkC^l@p==w=<1l%+l9 z@@Q5_*tzBWL*DZJ2e1D|*5Yi_tmC^J;Ijn}0oOjH>aAHx%ZZ&I#ax&lRo}@I3jFwLO|yE6S^Tg`O8cTN%at{`wRoz2)E1(0ikBk6lf?F<&OiOKmzYmk{6Uv_Wsy?i6f#tFq0#1Ol4aC5NjH8BK^-kxpwd?SEREmRNZgA=W!C3W(;4QqzB` zOi0Aka+viBOd<%S(qP!%Dg!aT%fie=MsR^|US3{}MEEgj1w->R0x9VkJG}QQ$n*k) zf<5$Pq@!>tY*;MvGhXp%s$0o_hAjqwXnH4Q&mwvRZM5!zj6|LaUA?S#^xT~b3oesY z2T%;JtQEtfr`M2ntu!9xdcZ%(d;>cMD#}*#TEK83?M-@VD6*;Wz8Nd`(yH6KZqZ^eGj?GKA$s5SR-J&*tUdOw2NGg^KJ0&(U1?!&Y{(s&R( z228EM7%=C}n8&vG06mK<^GU?U=x>;jRPI5-)5BJZolVNh0(#4_?;YP|JEF~lEUl7S z3TYvT?nzf+@hRgsPMUR8tpk^i>LX7Nat?U|gn)U~u=-;(pe5WG^c!`G61vR`LSa3> zN{J|*OUlFU_|~@dbHV-mA1w~w1w8B=3HK>#sitEDVn88>3^ zVu#HJ@Y|at>sR$({{1wHYcj$JJM=Aq;AW8Nu|497Pl-GY#lpNZWJGs$eQxdB*8BAQ zW2A%(7vJ5p+b(=mm16T+$X6|N{EV~p{;&C2V!YiDQej~|=qBxY@w zp&xwsyCWxUK>*Py^d7cJ8nhelBf{M60rC^>Abb5NVoPiuL8laBWAk3r-l8Q>BS%FY zlrT6RFt5Z*{Kz29w8!1QSbVgdIF$Z2b{xlJpN^qP*Yv~-r)*BM4&LnCnVBnqOslk=CM(L@ z^8fYv!qaScV&b*%k^r_{#7|dmuEJ$vgj%~@=lC8__P5L3enka0>#VSxNV0mLP*bm6~eX?ViB z<)rnOxtZS`%uGzJu?N-MVAuRC!wA~1P~!GjyDSh5{0fO`G%LO`TUJtSK^b*xg)@sG zG#i^;k!XHz)85&M@OU;BmX|3hNCQU}*ckT^o(=tssD`IZR5*$gEMNALdd0|TDku7Cs(uS$)r|$z=)3@b6wARPj+5e zUh;iBQ{T}cIDniuUzjGZP67$r4tla(?Ql(v7CKL?>WPgmk%NXSxauApmSi&YRM7pQ z8v?t5E(nA6O{h+>q)Td_%+VmOx~2QmX`~ZOG%z==i@z!T6tbABHFzPRn6GGbIwH}+;*d{*=ca=Tfb8-#|^A|b4 z4-i}JgciOw10%*D&wM6&eVs$C5(cs`mkV`Vp#PxL0MZR3I{Mq|`d&3n9Vf9H^#%T5 zdYYDaKMZVa?x9jsQn2M}cw^(7xuJ=P#^Og_#O}ecC=_-$paOS7g+5{7u*z*~a>vsG zK_VXcI=Ah9js8cSuV5`&Yzl8| z(nIg<`q_iJ3zk$3U9jW~0huW{vvn+Jlo7>`4~~uqVaEDw&r}ZM zkRq)KUcU^z^Lt>&z0OtE+RDTmOda2t`?^J$^G)!_V&^$M$Q7Pmh^enOp;SaNDy&uQ zljrZBM!=ACQd6=BtGLt5Q7oF#X5ZnZdx8c3LPl(s@kCSz0zbbahaFG`5PQ6h2yK*8 zTyhL_{99i>OYHsyZEzW1XQTD#MJ*vu7aL-kbk`LiA8G1;B@;I&j3h#5Uz--Qbrit>J`couALSC z`}OSV{d<3t`RnLtk8AN8@x)gofulftQMB@UI!`_I2ta!R9_R8n)7MZw;zre2D6wEe z9i=QyHgGLu48ik@;V<VqJV+?~QfekfwWIpemVd50&P8orFg>^U{cz8B zgt+f0TF5uo$Umv)r@@@9$^5d0A)BG@_#IM?ihE8owtU9qE}lAN%-CTRZa=<#V2GE%tt;K{csgb|9^Nx*L$4Qo@)hu>84{I82_^Em4ls^he{D-}Bb3!LZ# z1O;nzt>b3_?e%6DoQ7Atm33_}d6k@6>rtj#Jm!x)Zp-{bv1-(1<9yI=J=*={`zL%~ zpZUcPp67nu_w`)o`?}uO@FskvWv1pSxORg`8~P+N}dI~Xy5 zJLeL^ zA=*iN!z)_CT4l@nci-KKgJ{&#Q~o;UK`?--ufG!qvvhrFxn*`dn zOcf~U0EYxb7jiHHe(4M}{keq>so4Kg&^B;A_`V4m)gGO?Sp{vre#O%0)WWiie-3*Q z;H3^H-hnw1FftOZeIc!-weN?hts9jA?iEs+Cl;|X&;JeK(6Vc&Ec0m1nLAu6B227Q zaR7oIu8QEovCGS;D?h*@!w3qC;u2&ewNp0Ta+v*sPISA^z4^~EWk83FzFY^FKaR2I z9&*4vfovXB214Qf&gk3Wj=nHH0`gj5uHh>7{DsRM%~ci1;MjK`AuUkUjwd8ej=mJX z;-hxIw(rT*X*h??v2R@I2=g!$p#B9sMaM!2k>6|W`Q8kMfkJ)_dMrP<51Jk-^yl#` zyj0wB-$`2zLRP30|3Ti49ef|l+YvARZwjQ0VDLI02W@J6{+R>=5yH<3{Ne)3fY~Ba(jDw5`wCsS!(b1l(};Bg9;>rhz86L^Ae0A2 zk88{Ezma!&eE`)85eLD7^W8Mw7*l^2#%{@5<_B*_L$7IXWg`kGkS&gQXqp43G|2*g zX=WG&Rg#l}5FHd269Q|+k*&Hqz;C-Bg$(bk<&_aBa* zg^l8=H4nHPo{@SN9TO&#apF)-cZb$ZS_{h*x-??vPt5+Mq4p>`*2lb*VeCW_3$KOM& zfuSS1rk~mIn`LIr$V0UE^G{=#>g;Vb9D~v`=Nl%|9+_{qacW1l*>to%@A}gs;|F0D zies*xlwX4H-zsBu~@&8%Y z<;WORegP06a1Cg=Vh^}M0+NB-2;)Sohkjn`8wQs$fV!E!|8JHd?N84#=c4msK|CWo zivR_e0ILB_GQLu@77QRr?Xfz&8k+rl&@5|H8AMG#_c0e-af!_+LVBXg>J{Pj8`kF{ zx%Z%`gT_1t`^t2?&UCQJ693!z%`&DK?#^TCO|(<_3WZN27_Dx?tv-sYsIn_wBtkKVkDaeb%i@4xXg*ya^K* z40j@ATk+kKP*u;=fkQS~&f(w-i0y;!53$WYO`lj8r<&wzq zjY1N(%}^)xQeHxNp!LxYd-fv{$a*ByLWdwvIYC^QsP)%(A<0hih!^eb+IsK?=guj@ zYxzW_ps{5hb8bOrcA`ATXyNszTemD>+cc(VQsC-paxQ0IN=gWgctZm`vcSpw^M*1A z*&L-rVN}#cXHw-;py|SdVK$NM6u3>KT~zG%*viXhWuOJ-AJT$ne~lZ=D^0i_^huCa zAea{0J>n#)S_moH;1gI%$+5OIdqh|xEX&-z20@qnuDfE%E^ekWYJo^Uc<8EgNC=c9 z9!pA9Fv|-d5mE;G-Klpqjo3}F{e=m^+y&o}apbXx-O^&?Nqx>b+Vb(^iPx{^XL~}p z7h={TBacu5c>DPc$jZsJeEw`{W%W8+VZ>&RNXLbiCFSJ_qM|nmN9T`C#}spyMlv;V z-D!1o(Yg7A@KG;Ph4|D8OPs6}78YG{sg}vQI?k`n=YxW3zC+sS5v5}E~;7AqmFJ45a1x?^E z;xnY;V$&SN)Kv0Uz)hd*NteDn4ZGA8+kz~V&#PClIXPd@ zV+}RuedP9IMoz=oTT}czw7Im9z#**FAk>sSud0eQ83?tXm)fa#B*^*NNA&y)^^b^-DI|+5yEyMF4W>de}a7i%7Q%1e8V! z(}rVRqf?&{N)v$>DV7V}1;UX2#sk%jjj>9NKzmL=vmTJxG7dsTN}2wl~Ac^K*2d)WcZVj7m2P#Z{EZjEvHX9 zdvSf^i29UUD?O88S}+l{Dw|m0rmd-&(Ar7|)!_=C5R(Lj5Qd+^;$7j)F#XOEkPlZ&+ z!Ym|{V>Vz7jusa?WidRHQVc$Cox)u;LXZppLBt~5v_ZI3td%=B{(h4^Cm92w)gZ)D zQe1e}=s`H)w`UeG6hnG8>$IR!%Ud1W!;RID#?CU=PTceSSI`0?kBNWIf^I_smd91zD(`8xE!f~=yvsxLe#_grIUkWjK-Cwf(kp%!SQ*A33fP{P?`c?@$TJa6^f)~ z^TY%J9dim}w+$5)(}PzQ&q95UeVbPS-HPX4@9F2KGg(=l%wNE`g@x(Bh+fs9&r}Y| zm71umKRQ*;YJ{Tg1yrJ-cScnchda|K51EP;uKycaXz*S|MMjI*1<%qi%r9S>M@QW~ zsh1c7^5yt~M6iRyeYLF>{0YyDOw(Dfs`+p0HXf=*stY+>ui?5KZXMhgMTRte3Tlka zGdo)S+9Jd4?83srK*_3T2=|$R%!5KUF_qL%`@1FG)b`?KRzC3{q8pDIVjEVO?~jci zQtj^D%Phxs^*QKBNyT&!Fs=jwxIz7xC*Gl^~=}!1AE)EWcsMcwTa&>eZJ%4n^%bs`d&VV$udv1b%551=hWt##y zO;K{Po~pzH_kngt1;ui%m`KO>gj_om5(15w(a#w>kA7%d^F9F^B6gIvsqDeHB+{F^ z%9_@oF$K$w1>#@7nOv~h@$t1u$vgE5+r;1JTX{j@#2VRMyBd1%*oDagE5q**@{j0Q z_vYKDodRa3ho}G#%JcdE8bkhWFoyjYyDPlq4+pBTBFP@2*N=33L4Ov~`*jP5!&dbp zAJcNIGtp1aDwmGo(L?C?ghU-^Y)YG+u7Lw*Smo^Ex*;RYgM&Um(snkxWuV?5c+S0i zEEef)SzTmyQ0*}omzb+sFf3!$s_87CR*lJ2pO!vB$G3q1Wn?^>7surtyN~qZR$tQ8 z)J%pWp-R=xE_`@_%Ok?7tHw)~vueB~aawwMaU4!8+7VqaG9s^3IDk zHqG<%LM$nbN~M79cJq0qnyC6Py!u{BszPf^i+kMQ{P;bN|C{ZnB@hPsX|^)FwP*St z7ISz4g{x(YNc#mXPFoG2UKvOXcu(M#oTN|8tX*V+=x7>-m28+AFHL+jzGW|>TVVIo8U#xK zV+XznlPwC$@dY=fsH7yeveIOD#Ma}8-5bQnzL?oS^@^AM!QB-ETN*<^S>k-tg(Ha1 z0X%7mri2k__l}PX4jv{vXXcJHJ+(?cm3XMyR7X=25G-Srz)*|z?)BTZ2_ZjdPP9dx zhIG>}AbrB50v1!Rp}(I92g|ohvM6TizWDLxuX8-DnYU7!;6?^d^)ff_5s#R)L_fxH zV7_QMKQq$2Cz(zU0B$z~kNO|oFP7HQ-5sQiy%!HY>>+F1CgeOSCzh?|s;wE2f@TF; z7ge!^)w6v5_`!p@FRaXmN3b1icJ7sv&j4<%wPNb6MO^x^`}&mH2Uzh4&-D>Vbr$L~ zP`x_za9@WeSWm(C=zSJ1Dc#x5-3PXLNY8^9@Mhf_qFVPU-P@<7eN$w~@`W4_eNUZL30G?IV?vrQ#7%(j~s({6a_+1m2K zWoYQ`{)M3^R-Kj2TLE4q{|O1s)uP;=p-0?0_2_(J_)=9dU7!mv!`3R~jr*DGl~xv% zxW>_F(<@^ILPy2`ocX$x8sg*mc^swh61LwyE)=Sb^}`ne**We9diO4V%sia=;ia30 zhaw2og_t!+=E+q$1Hh27@+Hi&U9kt-sCO{x(Pq${2OB@&BM_h1Z`8zK;LA5-f+|>q zk*TRyzePqo9PCJ|Br64vP@a~25sn}}zN=4y@m+#M`6(FFT9v^fV9f#o181<|Gxvf8 zym0$ze`8KYX1#`%7P+<-57)wzE@5GGR8gGQm5@MmZ@|^B2pc}{CQ{)M6@f(xs)V-S zWmuDh7;#)W6d7U$p=dWCBjYbkb@gN_l>rq#1tWfGd^<+ESal@$1xSR{A03=Q75k;q zrCUaewNWvrmCQCvOG80vs3Ci~Q|J2lXX*p2L_~a_%oPOb{W)m1RcH3ImpyT(C=MDP z2YFqhW0icC2;b+7_!NO&fM*tKq=o8G6I-=dJ&<^~b!d8k+JRY*Sfc!b*sX{EWX{55 zSGF{+vZUnq#>P&Lv;>m*3s4t1d>b0&AEF?enq02eX$;q2KX@QoL}m-e+1Ug&MAu=n zm>4`3RaaLBW9CIanrR9I0_G>1=1Jki94 zdf!2)2fs6FAz+UX#JC3CErRU-9o-g!tOfl7f1X`I-$jsZ7U+JEh~NHw3%o)2ZBYK- k%l{+T|Cr?eonQztH_s~NyIrbPK*ikbxaBYTKVSLnzdht`LI3~& literal 380066 zcmeFZ`8(A6`v*QPs-w`UP=ppCYuR^|RQ9s(BYTT!zWuZkN5Om59?rI?r zH1P7smlMa}59DCCFZ^-T{f@HE3HT@Agw;Rr-+#G2Fmy*C*uPQ#J>=Z(DGxuC@=!AH z&~~x$@P6!Ojc|JG;p*t(;b?Dp$;;Z!-QLCdl8E39K@t8-b{-zC(n3Q2-x~y7+-!wX zPogssh)W3NySH_GQs#$zJqEo4mkvnY8y)=p_Xve8B@>dxsS737<_FD327P~RMPCFY;ugMEJ@k$APX)=*QP zqFlYV;L?@qJ^W}XbCkt0Dd$vO-sVlXSkIT#(Pa@Qzx9=2#ye=Ln)awPr}BiG;pUKc zv~vyRl3h5F+HB(#ad}NnGE@o{EY2 zqDRv?4f&b&R)uQsnNyndXMKcCA{J|QO#PPpN3!p81R6A2X^m|CIOxfhCm;7)OAba4 z-#-F8?QjWPx;M|VK;-KN)=E1%%Wly7e zDKPlM#6(J`y_qK5db!;pASb;qqoU|(UPLrm3A+)7hy%5&sDq5?!1=45vVG)C1O8c& zbjnpZ%J}s@M|#Ex8aByx^v>eW7ct^CRV*j24>x z{_0Sl<8C240?~c!&?V}BWkvZ2l>pkTcNY_^M#uoEM}D^ zJp;oV#(p!Z=Gpq6{}BIuX6us@2RSv%p>62={QM~H!4$X3!j%;z5M;7C=n-bfYwY&c}dBbbA zeBjt$NUuIp&4!p9xx374qa=M`xp-wI_Z)qCm&}N-H=9PeSX|uYR5?~1Z|_o#6vErAeRTZSmy2l5|_#;z6UhZ7YAi4zGh%mbM^iB0ZwZp44lA zSL^ED+WPpqzLP!e3{bU?dj}eSArSM4_4P`O5lZCE&8|+r7#yM?H&@AdGgSQx2BW@8 zF?oHjONCQ-b!SN5=bTBQiMTG$IbEK2g4?>3C0mBP;y!-bqlb-*lJvd3N5UGKY)VUv zHaF+Qm{c8}K2>tYD=I23=vN0jpzj~2FSGX&FA%YP&N@j0XXQw9=n_26C(&{_3j1RR z1yNDXP;)J9nsbpZ0o7*vs|lnXJ5PO{ccL<^C?c}s%dLT`=nG*P85toOX(?Un9(Nm} z$fTKfJ8||&yr`zRJ2e3{XI4=v6P}x5rxd*iy6Wm0QWy357R37&^!gn24Ee)`_ZA5r zOVu32A@5*U#nT%TLI;@P)p&`;zO{bdE>X`}9qW~bn5#o22I8cbA~)J|;56+-F%(M0 z_Kval+Iy(Ae+j5-s+$c5~y{WT3qe*V`>$wLeV`H`+yIYT|r>gr&bX?(Uu|M`;Ql0{{%zgkxB(|~=G8t&{i4Z%zpx~~5)hlbjIXXI;a#Obc&d#i_U%k{d?|W=}Jd_yjB`iJHKp;NV zQe}v1>Kf(q>-vvKCsT!yqK7;Ws)AT)^z?*Z-|d99p*{Lp|1-AU($4jhX|AS_y$x4E zaq*&@^uz{CsEEl`RFA;8k2qn7H=)^#ZcEU4027WADM}sv{_4o~R|c;2^{$ghhqd`+ zgm}zPJ1=qG@VcBA$7Q_M4fggh(2uX{^~7XY2W~_|C;wKNT`Qu4PG)uS>tZF>3++#Y z4b<*V9ORhq5zX`KUd^_Ns;4S&#B$Ox92eJhA`T7lhu0O<*H^ez?mdz+%%9}D9@pq# zeJ#^fG9P|wV{;`bEz0ikWw@z)6iU~+lm?pTvstc@vE1zZIvR`Bg{mfR;`7mHLnkLQ zN`PRc$wpOZaAzo@-3FbKQQ3!446oARk0)=9xwB5v_q?!AV&TB@@1JL#i)|@?8Z@qV zJ}jdyoG{_hFDi2Ok!+1o#FN8_kh!CbfO{rWQ5U5?Ce*BlZf`X#5mUWu)b!Bm2U7(D@>32UTFOZf)G0rTcd)cHEG>N$8RfOOdR>Sof$#Q6f7nn2 zvzx5U$O5(7!>?f$zI4T9P3pJGoAW7C$ougBX8 zOvK5%Qye<(?t$WyOcBix~?sHMW8zv@RfzN?~0>-olSxu7z`}^YR>Y-v1 z%w5Z_7R@9Hb$7)t_>HADvu}_8Ln<$sp3ne7C0u6Hgvx2Zd{Ys3fr8pv+$2yR@P^N+uKGg;t#6~G9Gr%JF3ByJWlWE()jJ!5lm8Qq@A;1!Lw)O&Bq>Q zYf)k^kJ!Y!>KOT8^as@j^90QhZr9;njLbB6&Z|wY&s<}Z>n9{Mbj>0)rrN@-OAS4y zACihp>TaB3lkU#;@Y8d5ze(9!oGa2|2B@T?&Zb@qk22Mfy+wPaOnV%jRfV&}(;;_I zRYc?wO2s?5r_#jy4DjFl!{ zp578JGw#jS+Lb1^^L6x!4UEiOvR{`N@Y};Pv$&A~Qw_+AWie-XonY+Pp^lA2iaFGWe6Q{g?Q z(lqDkOnC0W_t`Ar-o3-TOwBAD=Kbwg=m93fXrV(sJ%iy5eWsCWq$69yo8E?b$djFx zhM{>vid~zkiv@D?!NGh-VsA_#$WS9!wbDcbKN5sqteRtE zeg45GKo_20P^~fWA!et30r|7*x`!3IKChplk8UV-o+~0EA8~27GX(o+EMa7?Mt-|P zQ};;iQot1J_AlH02TOrV{sqJUn6eQOPjNWk@fAr?IX1&hNe5zpx_T56I(SaSJUoXh zysB!K_86#%Gqbb(!NfSYXAm{cn>dk?rx?aS5fK)QUIg&q4;;I{Q{Y+O#$ec?9*{vf zCPg3Ir{b-FxCT@ZCZmJJ^w3K8qt>Ta8f-l`93QerD>{2Gq;tbvcu~h>NHYuDf|g`T zXQg|yU#t7QbEmk@y~-kX6nm>Omd~l0Z@W-Q6iIiddi)x;{l3EXA$2wWCS>)p_|J)w zX3-W;RxgtHV!va&6F!QbzMSv8nr!KtZ?!IylvFAAd%;`b=`oKDtV{deX>gOv@zqM+ z!@joF(O$ZtcdXM!YuQ4?!BBr{uk}CP%`7hXQMaODi;9o?MXxil5t> zMu!XtQ0CTHH$Qt@4*H+uhvZCJH zkGFYfv~i!KN_DkdiIIS`w_uh@QJ%32W{*i1OVY)Bd~;XJzbbOWjft6=91|U&wiJA@ z)I3q3vyW=F-kV7e(h^KChJrIr)%3A8wm)PhBHWn)nZwo`C8)j$?yi}5S5I=}!K_FB1c6p{b*VQ$`h#jMG zkIw@PI=&CbkDQC}WsG3O*M`;Ij*O(s$F!hPs5jJ#oUt$R-NM_remX;|uCBCEPnvg6 z-w9sdz}KQ#_LXDnEjwg}SJ>s2zZ@7h#5uKk9llj}0|?$BLktsWrtT({94_0N>e{&) z0uP|>d7rse13>1%Jjl*w6cFN`4ZH|QpxJ$33YK{<>E3+;r?PF^F@Rxc@JI6ITKwfk zQ*ow!F1tVG$X|{GR|$IT^x*gL&Q&vc^In8i%E!q`%aQ@i?2^j>&RtYOpDBX#%!T?f z@*4tCh*306(pZ+E4SE;$+hBuE~}JQ{FLT4v6C z=(_IYp?w>n-hl zt3gietSx+xFk8Q4EsXY?l39vvn~EC}l-sO4&{zuEgnR1h`Ww}p6Ba&TRl2a)cK7)4 zBd_aipaCB_pl$WBo?;VjU>#6m*G5&cwb(Su#0Kb~KJj>^=4NFo&;EBzRoy%>)FcHU z)pR54>iw$CmVo=pZUigx2K-b?#l z!#zD*ls!Qhb=S^ZpzM(cs&-ZrYIR0-h9$_c3A;O<_;dw@jIUq!rBjh>AY)D`4$zjJ zs%wvw*x&`R=O_I!ihiV=xnE6*|;Hs3pV*EXdqm_P>0Lp68@&oCXhv@=wd<|OjEKGsou zuDsTTK3u&aTK{<~RnWJ&q@48?gX1kN{afl{d0@5O6CcS-Sdt;ekJ>~+hi_3wEPJ#z zU^ZYxQc}T7TPG*>K1Zg|y6cP)GU>VHtd_+ph~EDGvc+;34*~*+d!cVqdP6jM`!p!mLxTFFIw89FeAIJj@Bb$!m;#y-(BPTPqzEVLZ>mlqd1L&?*;_^YL@L43~_N~Ocj zsHMf3T2EUS2`v*PbIYTG@C&Jfy}06FN*1ub|KCg}xpQO$m~WA*q%Nk=0qOR0${jv; zfiQceOH|@$2z(S8?d$1DnIKDV?yA9M17o3i!PjIpOGx!x*Z~PlBu*sgP`2l7WegFk zjxb=jH7>q=eBN(+re58tSpd}}9q6YV>rqE1x8t=jQHsO+6G^0xj^$t_P6+5yujI!q zYJ4PKkHpzQm1$WY`j!Oe6H)(==bcXF=U}5}T{9_A#=AiY^IquJ^(8sES4#{|(VZ$I zO{g)cX4AHdW+x`vnYMo7LRFncY=guZ@>05zkNjEAK@>*3;{?_OC!@oQ0*zD3n}EIe z1{9W-xbZ{qr;?I4y$S23bo9r@Hwou}0#y8zg_FEm{4GDXk zM<5vHTo({pYV&f=-#GL^$M9W8&m^d+g2u*n>m__u5{a_*7|NDS+GD|~8)tATGSaiz zrgV0f(q3aW44xt~n(8S!8mX$rL7O&ByiQ6cJp4LImY(pEITvla(Q>geLIN@6Mgui4 z?rH$f*ofo~R}7B{Z6YovW1P-=t4kE6F=d5gajTq<<&DsSr+FvCN^Ee*#p^0U^ps*l z^d|gDJIgD1C=^k3cnKg5YKESDeh`JAge&aE3~%?~=bT*p!-v;%@Ux}qdm<-*^uG0*Q3fo9YDz2}Y+%cFC2PJ?%!{V%(R4o@DPK z=kO@@1(dxCaBjk5v?tJ&Rku6r(eC>F4Ym7ZmucWB$A; zF`t41RV?lLLA}!Ck!|bI&H5!>m~=a{2lUiYq7B>uE&@h3#(4KJ-k1pIBX6!F8#KdP zP;HrHP7{cseZy#ItSX(ryXioSQ z0J3BGrRWAjI>~$Yq&J8?Jx7ikS>srek|P#w%f*@BEhPFafK_J$6_#E1oi4_ z3T%b^Uj>8Cn^9Q*>-6E6?UWx42|7I8E|4D5K9+B z`Dj959Kl4%?xJ`K4DWn()NwdU)Z)9|AZfC5az1-Fen!udk_D%Ea^s z9?2t@rC~>&8Z88mGB7A$6#@d+M5Sl@<3yZ(m>ms?_1JPvh)+1%WOEW&9Si$n#h*P; zs~}sz$A(S<*-F{nX7b7H;;J1HNNDED?Gg;HGjiJ5TMl0$LLVA{;XdFo-Tq!HKdWYU zqP8niA`0}kEh#OR6PzjDn4dgM#>68GyOB&%U+a#pq7@F=`{Z!eUAO*Kxwz} zpG9eaf%AgA)G1p2Cdvy47YNH$3VGoLld5&RNu?u5uLVjU+^DHwIGo|e?zZtwNrOIK zLV3QAkcj}o5r*56w>{LOWBDg;+x|B=#HSwPkn|SIn{YA(q|aLp7D+j7mCwN!1P%^H z9$@oZ;Ig#g+A3V7v8RA?0sKR9Pyp_p$M0JMBB)Ylsnl{$kpp^m*}U4Vh_)I3k)3i# zt44@Z@|z8QEe=73-b3Ul)4(*XYn%!J6~;T%2Rzu&EnK8)zS{NEi{4AH}*(mnF_>ir&lUSC-anD@YS!OjyCbHaqW z5lZ@=gElBJqNAh3HIMd=xOfjPHhKL4|KK4{Mp4nEX?2=?68PEBPL)F&&BIu9GZt93 z#f?XF*XJcOr5G=K;LFl2>KA#Hr)hcMNiLW{Tp+QPhCTbM#8DO9Lr*!WT)T* zvpY_K`)k3NaQ5vCkPA%pmVc|Ot55Im1e?Ej@q#SRg0NuSs+EM!E$`^iXlcDCcC+~N zWyHIzTVHaC#LrNKFfm5-Zj|VLwXpU_LlL{UH0v%_E1ddQYyv8LY}Gg@a^`EbImw%# zP6;UDGi#V~Z(JznJEIKT-I_GuERp$;2Mooi4qWzri$vt6%ve534b;vOugu{?UQzpFT}k@@U?qvEvDAKzg zn2>5tPR@Fx=6;6!%BzEqBDSsb{hs_x5%=!PTvI-KHer~lN>7<78q1q7Fy!k7e7)Gf zIvE*!FwEbbhx+=pDd$gq{?D+}Iq^M*Z(%oQBOZb23TOg-wm1|2NA)@rA8ABGQjb5r zV00E77;WvxfSzE-JOn5WsNqRy7EEXcRd|&!RAB6UK}S_V zTYSE)Y^fjZzSF}LB)j_aomy!L&DNheU>SD4QW@YTQuE!MB41ZmJ7bl~g-D z#-x<_H;6jOgyYh8Z`D$gT?WUx4kM7yfQE(~J3}8%UapItJ;Je7J?jJ*=e*aL9-z|I z)kP5gTwPhIoSG^RUIOGE#P)|4b%7V@lYx6dK51mwn3tWMKRNZ!U9Hy*`Vaz5xb|*1 z&O8tRQgbe%#m-N<1XP_eJO+D6B|(2Y3&A^=)(SEr;+rRs?Fj%!ElfSS(--btn_r^{ z{#qamp85(f_kdFXcz#VyBry}hpIk6xAYq{5HF07DH&wy(U@-@*yW@xTdADGTiHLS2E)NV}xL+Sx;8p$M_~b`CI{hAO^vTb^GFq3`&u$vWp2|EUsvqy6 z!@Da^xRIs9JGVZgGB5|RyveFNfO8bb5-B=3z)VwK3Y=p_H))|-C107I_>g(8lgkiK zD{Oszxk`zJ4y_uCQe-t_7#eB;`%xv^P1@VpFr40VVK(uhF*uOLy-2a;h`U;ILA4LG zK$jdoA&P8RPi)&A@!yt3AaIJH4Q?HIDs9?ir6)0% z)>zmyGidU=lnr94y{(2gpx=C`CR4W>lkC)PEF`6n`GDW;z|<)q_ybP9Aov+Z{{c=r z?6IBejSpZEP}{ip$(~s4k5Gi`8e?o~DhhGqyqd{|!%n-RLi@;W3p1)Mfin^5yfIru z2vU9WAZkvX2{CitRE?9GY_NE#LD?N}}b9yPk)~ z!+Y}=7R(@%qcS>f&DJ^CQgC1q#p((z>Ye;#p9E@`CRNrjaa3Hm^J!ywVE(Gn#S1g- zfDQ_4ftx(1E6V$4vE22R%0TD=zcU_cb=dit&JiA;2ez$uq;|t=N%zHMPY*lTA{<0G zsvc_*F<1nlQhvKj3I}W0?tzhgdbtCNx894txJFvK z5enU>o=ZRd(gHDB>FvP^qetcbCK4hOhbn;nGdUBuyT<1wm$cxyngzUKEPtKe}U+lq0{M5zUJdCZDWbwy88h z71ZCoQluHQS_+k7Ozy$gP+r?MO{vaJZpabDIz~S=S>7xwEBlz1W&_=g?1@ck9ETh@ z7E*@MI(@90j7J`trQ}!Z578U#dU5U#2m~6*bfMY%3Q64{tC=D)09HG{zDU*Iy_nvX zJ!z9*S^Nf^-=?vrKNo5?jOJJ$WjqYXLE%@8-qh=n1Q@{4h0yc*={%iHuo>`WjB?2W zJgKeum6flS84xT-4_$)YobO{R)1;sU-1g8&wXwpcd+jAOF~=ofYy@_oynk9gogs{{ zkw)u=K+@(@)UX{Lg4D+fQ8VzH-zBI=3kx6yq(bYI!D268Pv}%lOpPos;0Lq!Ljy;8 zOqOYKI-Z#qEF5Z0P&L51ol6V5OtL_pjxe|=_}#sw*;PIkb#TWEPaYl&9o{VVb{h%TcG+sBc@%ELESDO;P%2oT2g2kvr&kH%n?^>Yh7xt z4^tYJKEQ*2u&|(yofJ8x2m$eg8#qjpV)8XvXSuhni|6;>+$l_ewkzHFHk$Qk_pmOr zEiOR^)(T4~Y(V7xZrj0~th_6aNg+BE-dqB3ms?_TV`SV4jJOi!cuju*S5IAXI3SpDJ}=c}R#CL5m5%-u@~HVf0Rtz^J&nwtL? z!tT_6>3U{!f(BU8fOMh$&6KvhOt9hP8=7C0^ozFGg?+S zyVUkY0v#e-1py`;>VZzbGM-TEjpV)3Rru?Y8fOXYUhLL7JU#TYo}O1xfW9h-4Zd>)j`T(Kv2S4g%dPJn5t*<2-as%JXj7kO+sf9; z5#u07{J>?x(6*YvTzhBd>)jwy)N}tr+oT-O-y$bvc%O~*FA>Gqqcd4Hz*+UJZZ+L; z7mWFt2je=k&b#HnkQPx9@-ORn#2;VwIRzF)2#)^gq`A(M<~{}x3P^h*Z)?ioJ^%w6 z@|*izRo=QF?ej@7T3#9h8quxtV@hjtEpd8!nK!PH@0`l)muKYy$ItTdT~NAm9TXJJ zVKa5Lqt{Fa)(!-FAdrNl)5zov^s{R{cEToGYXFOQ$BS}t#+w3in_;!JwN*=uuOoBo8`#QorX91=cg<*4tl# zkO7fFP1peVYA_ALrxYQxgc^h)m?~eL1NK>pu_y&5ncyuH^L>qH^znSPi6+5wgsu2airXJD*q<%c}X zF0XT)|LI61Xxp_ZMF_5^N;V!}43K|sdX<|(&RqCux@pR9(Z@+XoGED^MD`M@O#&X-bS5}|F4m8h6GwBWy(hAHzj59PU``upot%T?pl(OtpgP;8 zI@!E0_OJX&pPW=dyl-O`gXpy@H$hasChsga{k%`u6d86j_?V2Vd6rdmYeyW6jFSav zcRwd5-LGVY3<~y5c7ryH_%jb7x)}~#LLdmgJMME3rxXroCfsOHD%Yxi)3kNKW~+3i z2|*+oR{eO+D5D_+o1AQEA+PNW`K?4mPyb512IrASy-*-V_(9={Ok^BH)3E18>?{4s zcPs=YQUP0sj1d^f)}5-F1R(!KHC;cwtT%Q3hb1b59I17PM;S~nz&{5Fwg9Nv4nhM& z4q1SZ<3*v51px}zTUKsAji}Gt2SF1#6(!WwKWY@k|J@UE@+h{sk)E;cKa^DPs=<|lIZ%MbDzybJ-j=#36HHig z>zi;jz?GB~fD?{-APO6Q+5OvU$p{z#9^((9s|d;DpI5cP#Rm&Htp^$eDj(!=?4X}E@``Q z%F}Bb7L8zU5$yRnU96#C0eyubKHDA-X)9V(g* zO#Y=0{00IVB$D@+uSdV#wT5zc`hBx%>k2r&h=$=Kw4&Wi^7Z3mw zn*>xnG;BoIv{1K2$!6+}qn zzlV_V>eXLz8@<=7XVh!8t6N|zMj?nGE#nV)^={4nRxl=`%d?w&@3dNi82k4dGDu_fiesdg-H2MjGX6hm(kc!DkK0Kcw z|95bJAWhJcGrkh@2+FE@$T*IMc1JllIGMVAmKr(p<=KW4{-30rd?GQ$L@*o_3oO|r z6fUk&Oo--YSa(wzLviDFV04LUx5F+&1p`XI2VPKGwX?X{jP#uCEgpb$#$)o=6F{Pw zrGx#GB?j|JDW|SOS_x$3$Pp)qNr$y=K~Z>_TmzB+qk)-IIh$vuGIo4D zlS6%Wyip2QcCp{>uNWumnemY#9Lpb?_WVt&Tf@a%I)EX14M)*qVQsejvsDiJnOB7nR06!} zcpkoWO5_hGc?;4@W4}TGmi6(tHMsGaje+nYqlM1?{w&!2ag`K^ zZt(5n&AXG4*Vku&6LxMf8tB)EzcH^5yuZ2{Rr&hl$zpXOci_E@?^GrLkbp9Ell1dv zQ<5W;{k%NQt@G=$4|i+j7rIJj`vf?(TE>B4cqii>+)fpNK&~QH8<&=@&Cc`ZVOny% ztLMn8J3>0&U=NGBn*&2MC+7-ns1{7sC)yb}SmI@BHm1)^cY1oW=wBTQW)sOP!pS)K z{?)5&)axH_^o1?D-|>Wf9FTTEH_nR(+NeqwCV=6TqS@LU2%cQ7jV-o5N)B%Kw zZ1RKI+PO@?JoA!WN>(-oJm}n*S^A{D#wDN@s3x7Uk zbWG_%oCbSYIemS3;Ef6ys_P^UCFlzKJ`NhGCkBRHo^T6Aw(rc()~Ff|=&{gq>+&W- zmX%l9C6wEh14_a+IF*(Tvc=JwUYMfjJ>l^Sp(UVh8lHMEiz)HK=g#c5>1>9$*t@iQ>kstC^egjwMwM0#2Lh=gXLTqc+^_FD7Yb598WOaC@w4GMB~I6 zrGfCPs`3#jS~%d<@K6tt*G1=Y6;gYk;YVGys6%tc*E}=R6lf-V6~Igk*c9u_y&yc; z8d~}DVJbOb?J_az1GY+}WcV)ARI3=Odv1Sha)0Rfs4@cKrpa-&i2%DzC9o>{_` zm@IZbyE_2+$=lM_!oHAvPq-<;3Lga3<7BfF(t^%IY47vOZaLdHX8}A++vQSyxs+2> zODX1o(bcQZn)TRC^P&bCpTBtv5Da;Nms3q5tj@r`#(KNrSyh zTVJk=1Nz)_;}l@mmo$Mt%R58Vkihzy_THY9WN(p`Fl^*rs|~i+?~nF|HTTyIB5f}N zQD1}-_=*V8BTH3widdJ1h$kDF_YOqPA*hM>m#7Ih6y4|I-U74rkHJ`M@Luq&E_Rzp zM@?Z>IAi^0^WWy=##PPa$m7Z!YpumDYEU;8U~`eg8w~cRUte<+Af92%ue;F%F8X?B6yHO;q z@M)MmaQ)F9**y3*@H+8iT^IiWW3ao5(LrXwRg8_}AT5&i_cDa#OOy(8{3!$eu-zbi z9Et?f)!F<&+{H`$in2ak7QiETHUKQAGPVBh@0qN$a&Uz$9WXvbL{JUGj{YS&iPydV z+^wa4H6%US9@e;=oKV*;w-wzoI@wTeeCWLf)zpcBbA#D5;P<8lR2S)^B21IrG8xb{v?RXodU&c115I2$KQc#DmF-ZhxAWa^adJqBU9$^@*x z6|8*#&T2+3I8f!-ZpV|m()Si$)!gCF6&sed?FLI^&T(k{@8tjoO(l5)39} zBhc?`Y-SL}!6lFnN;fA*pH6ruE={HMZ}RHqtL)e~6ce2A1F9|>LWalBybrx~DdFv{ z2Vu##EY6?ddVAp#+X*_6QyGcAKM()nysY?JBg}v2FHvRb>WYEtekX#HYLDdtFR^Ax zcWO`}3T!_wBx+JnF}zgc^EO5?~E$MUz$))e?Unb7Wh+y1HhQL^1z)qY+h$VK^7jT}Ag zhvyj6+lE~?`y79GvRNcZAk3o*kav*$@k)<$=Dp4a)jkP*>vLTX&DF_7%gE1kReof$ zRmrfY@1uk);^D92jqC`tHmA!G2-N4lu}{V(xvB)q)+U)>dbW*;E? z{mZT9ssVmfNMR6tTN}5}1nMtd6pMU%YB>9BgJqu|cT%(U<#Q@=&5TbaMT>u0b&5!y z|2^MFbMM~s`H>Nv(<%Z@WGOZ%y1Z3CCWXXJ)JvFQ^?F9vq?Y{_NG9U$=$IC!d>y2y zgp4!gkb9Ed&cDFK6#IF-^L$`n3-w(#PrtUNK8rYP@lUnCj6|7Ua?8sW8_rb0 z->&a(B-5#B2v!~l*4{SdMf444I>7hv@LQ=Pz8{N&l3QBg>o6R?yDM!awrxSeiJeLv zF4iL0uKdaZ59!3UDdrf?SE(|4$yJ86v55E0s1xB`@0!Kl++z!>b+3!;oG96lvO|`b z;e0HL&sWX~VmPgkyg$aR-aX({G*8z%<0f#}!t@_^$zL8GjzzyFk??79{?a}x^*6b= zuE3M)ky9YC9MU_7Dz%-G^7mqIr}bK_%$>`5HKdWtg~-tg4MUvhpTd@wzUu3f%*?#( zwScU?ZmCsi;v~8AvE$UMyX?BAZp}Sc&(1zK*N$^C4u1NHk>5IRxK}9dC>(^V``?E@ zz`>k2%KE{i=uGs~KMn3YuU~6$kFMEg#w5tRwlbX551L4zpv<4SRlfOZVc>Pu5yw)w zL_Ll(wynS79@r_dyL`U+s@~FqR8-Dqv&|U6Xj#m!hyFXk!Jz@C#w8a{PZpMA8+F;A zBvZaR_QW-#?3}#{ zrHu^Qiuzj*TVy=^GUI_wA3s1M{_p<_aQd#5;k`#z5MkNL+K98V^aYY_hE?$q^`cL& zGBy8Ma|+mX7d!Mxt0N_=&+#wActKj-}uq3qRR-?>*TT)>j>Q4M?%r#&eI(SsPwf*VjKgj`vJ!deWHS%giUgn6JsJ&S>}Pf^S#;Cw9Tbabz!3!ROy zR?dwG;R^As-k=+~%M$4jJ#C2LqUY$j_{_Gg^=$MM4-+~?@@8b8qlH~w=&difJ7p)1 z#f41%BwV0R?#k6%s~3|0ni*XaF+3~}7&N!L94=3dCX+KVdaRw3syP1YL zIhHa(QNzZp%azAg>*7X^B#g@CG!}DQN#0b~0RqWODfNjp>;s z22qteOiX2)=GX)Y0fdI;{p;s=%Rtne!OBbin_h(!Lc-KVt}{|* z0Z-qU6hY(ufunG`>L!!Q1fY zAyk+;Z4&k|FRRN4ZCz||sO-*T4>_dALjJmD-8!HkG)`ebMX6x%NY3AE&-c_w$rq1 z`@u=ka-qES=uk3AX{+x?o{52U#GfOaGnwFHugqcSk*p3{2EwG^X?`-bUV&d+`GEd3*K*$4)PI`y6?GXh+%^gl)Mj}ep@YW#c<+KMxYT?`U0wXM*+BeYfH8m4>Gy33lHP(3_;YZ(Y4UOn z%dT(t_Yw7$G|JE8;~m@$O$CH%wyxZd`doc^pGavB4@y(mdLeY3HR%>=&AbohE!d@5m*1WAkgqh3Z%|*fmHo5!RTH_;ogo9h+##SDpcQ5_^K4m;Y zBn>TGjd}kV$y3N|Q)H!7y}f}Lti5(n2o8!dQqQg(seRl*xo2@>x|e{D(>JgFk|?rP zG1Q|=()OnIo1sCwNe#gp|8#SR%{YFo^3kbdY~soJPg~8?GIB|L0W+%6Yr~uSvhpLb zTm6HBO<%s4+1PS#{k$*#1HQVIFilp)UxkiWn`C`%E?V^tXVfqwYJPEsc(gY8(MvbN z+h!C`0(vW8!^u19FKSb5Up;z@+Earo=V+}hi-DoA>k^%vjg3-#WF@#S4kN{$E?Y2A z0$z=dUh1%mN9sOy6Mo*1X-!{5Jm;6$XeBIpk*{I%m(59y+!&aVlKx(4@XoImua6w} zbCvuxKX}2yPx?o%!9N)pk=DiPnhk+3DfOnmj~1kjv@ zuP@~IqBQe$>g~FVp$f1Gl?VH59v-4YS&Z1|F8j*~T`!kZ*=iByyMH*=io63|6Sust zr>4sMwBZ;Bcu@_S&6W3g^*h1gM24pgINsZSZg=K*~1FkzKe+vhm7kTebV&YbbTC<&~qM$xB zXK|kzY?u5l=VEsO zCoPis#ekM|);G+_si)!^)t-7;IVjI*PNlDI6%m_o)u* zmUdg2AL%lN*Zmq87XPy4^AGe5a5o?@(~BFzvKOr!`OAtBl2R9FKIArpYjAfB&Raab ztSp*j8lO>q&LWshe@j;v#xvbl7bov@y70P}Lp4d8tMV@yxmk)pV`av=N+OcDYIpd6!f9`(vGFvB$nag@TE$O=gG-6Ktb%5Mk>%_nv zY2TUbr=UW7s>L0}&yI~f9u9D4(IpmX!nd_t{8t#t3hxkx?h5K>8R@pqiDh_NS-mVT zXPa{tQa-~T&46a~^B-8IToe@>x=13YJ`m2A8YU5#PEwm;y_#3F5{Ya$nV#OnVr`kHCZlGHJM0 z!F@&wey=k=dvw0kC{i-j2BwXO$jN|%9nH8#tLsdr9$xg01#CDYOiW9B(e$EwC4cWi zviqjfOVyzqhsWZq03($*e@M%*)yS%ohBsz4ESbBVOH$jucUxK9_?vUBe4BoLcl*q^wUjkE_HAY5 z(XSS@-&R@^7P+N(fhx zPqmkcV6(tkKX%$|A)C=kfTZM>@70@LaV_!Z*@!Q1g)}yvfZ1F{s9q;iZYDYprc?YE zE5k>2s)cSyBz(YP6Op>3_00D#u+v+@R9$d7Ci)EGt>YIi7+N_x{*5L+$kuvddF9t{ zi@Dc(1qFVucCfyQ%|-c*KhA3IUM@Cx4lu{CurTy_!`{^B~D;fd$*-}gc#U);X$jZg5X`ttwq^c~<-_wWCbCQ(*0D<{bw*{fu)kd+;> z9eb}zk{yZ=Ldf1bdu5YxtV8CpXI9q#K0V*x-}PKq&(-7Mob!2q?)$aw`_m{M>F;ln zifFdx1vp4UL#z5A_d}CmAN<~WQ*NFxE4|Ax-*)}mLW>YMd8D>bJ_rK>O}L=ZRxbRy zD5;V;^pQCaZOYw0G}PQEPAnVA2pC_a`jLUbP8kXA&704F7LiIyW<&{iRfIpPAEj`t z{Ul}REO@ILtpIY5c5X6i#T<#0itJEhdP(rEG~8P-;allYd5Vgz-Rx-AHUf))tl6FF zj;i4wKF=Hf(F`IGULs`P!R@u=bVFTz%VW*S$qjS^kmPk;w{;Ccl}Gpa{io^C+Q0mz;%sHaeI!DoTYb_U_5aYjsB) zRHF&k@2wGK(ofJ4gcDOWp8Yflnsaz*^89W*E8rgkhSS%)8Zgr=3`7d(MBX8fN%1{w z{fLt7O@88??UVk;gJ#Q(P<=j5nxJ!@^4T*LYhFbT{cGy#0?w9Uz(>~6xfG!uk?pwC zc9fFm0-Br##Et+P3;(FM%2(v2u8BlMw3AxcIPVPW>`)L0?yc!8nV{Fd4M>Ja=^1-f zmX>~kG&b{uGpa#7ygFs@Ai($FD)yrIkFQNG&m)7DmMnZAl^CCqc>FQ0^QLNBh_-`o7{CuD&vqn9NvVks{8Fjt9@a@*;)C>ot{)|pBp ztQQ3Exwp0=p`xbm{_>3pSDiIgRQn@oX_89aTM0@G6bjp2+)XSj2!K+`nSbE#yc)H>oxlc6L#t4a&DFToS4b#+m^#%}u={iOMUYE|EZ5=^H-6!M zD7}2~*-c0KTtKFr9DTr=@TF1{0Xt_Xbw|jtCP2aX1O#=fi@W!OOpdTADqb$FBbYtR z{_h{^A{P5aWp>D&NSK+C_>22HVoD-L*Ak_xC;|69kCsU*nUIxKdEeGuQckp0nlJL0 zgTHunKO;l>5C&-9_IfQm`}60|#9K1kFqi<{5#DMh5k8!Y(yg$2T|VitV#>Z0*ViGY zHhoHhMY|t#cEV(^bkS89aG{)qaqX@GUp4wI%Bl@ncPQRlnv%uTU`ULmhTQ@t%i@ND zdWP04PAa5WyvoAz?J^!U0YQnbK=~%*r}&LB;v+p{@BE@8-XU~HZ>kGntG3P z<#BQ}tl&4|7FcOo%@%KLjo1mYc6Hy=(vopl*Zr_yD#TuQOz>3fL35k@O`YzY zHO$&?`m=kTz6(iBS9J$LCFjmCtpHb_xjwoy`;!(6s8!1|RzG%Y<6~al3TRSbQU_gd z8w$C?)3%Z7eIG%gOZ*;xJQsH_I0@2ZW!WwE`P3eE=TAEcqNWac>x~MH%B?m~5%kg& zbaZuTdat%BG*0bBbgKTf7^~a=V}Pn^**r5_!dq^@@*YPbpgjWl;fS|QV#$PBoUGh{ zDK>D94H&SPu75o*{#Bp-MyTl(e<=&3OWzdTh??JeTFyNjTsYFIOzN>;Li9<`U833u zM_JiEY~ke-vaoX8`MVPJQI2fJ-rxM8{Otl=>XC9=I@pGG^T!cdtae#@(rbr@)@$>1 zwg-)lWT|$lZk|yoC7<4-XKb3l7@!uJJWr_{ecfi9rkcqqmLK{fzWZX)D6ymCmedt| zT>~sbhnK`j>0DBx`@G)5vIsaDg1YDW<%+y2Co_>tzX8n2r7=O3WR_U#lotolt-7m6 zX?vya)H8I-{b1UaOVA@4&T#QD>g;b)zGbKtP%O-l=Kb#)WO4B{!he*{idH`A=NnJe z5{A(ZX~)?PiC@eZ5NA?1baQ*{h#?0U1oBmy>g=<0b6LP(X1Ipry;#fPuTp)fO^OWp$!aX3=bxKJq zGLX2Ud71-B*z?`;#P9SH`D_v$!}gZJ^x?XpLHtL&+^5-$O#SBXGBX)L!-e;6o1fp0 zXZ>(_X=$r}bo6a^`kJeU;IJE8MJ+C*%BX===pWWa6tPV23&AI8RJf`xW|o#hc9_F} zv*2(Kl*+|jL6`ME`ISTfBGInpI~~lu+YLlMPA#)gG*#HLyLxSNYFtD}CFeo))%UOn z0Ug4wz`u=lymZ;QW45q=GEV@}Ilr7P7BK$0s);WNH8BF&3ZVNRUIv~VRU;RE<;Cgd zHTS1t4o{}e&Pz!#96+lVqV+wtKRzw(EnZU+&hsX0ImpLKA+eV){gdCzSfMJllLodp zT)WX^$uqM%28E#f`16J~G+w|4xw-2raaR@`o(tH04KU&AJ#hOBXiPt!{u9$?2Qvwj z_=^{{K`*yn9O)=}i;&G0#Cc5e7wE+FNTEH{>--;F4Sq!Hz*m=6tQHQo9+;ZVx(V}| zJfGQ|7Imm3hH}ltLw1i^$#J6wec(liyW@jwYOzx; z-|hRp@7tZcrF3mc<1KJB)IV8kQ0?bxkad}g_|z4v6xlt9{1&=1x@Ouj{~RO{pql*y zvmkftYM!(6@Hp9;lHWR9#q)b0p6v9*Ym!CN0Vh$c?tNC)pZz0L_unqePKE9|56bTB zBz*g1F3h?BN zzvkq^3k4mZk(;o_1ad8@*9>blylEh1ujOwIqw#UsKt9QcI$V?%YZ)A*hDigl8=(wt za}o4ER`kTe`YSwyR$YuzFWd(G*^VD>aH))eFL{73(EH-~F{3rrp~h{k?U+s$Hp>&y~v}P-O<7$2bJSJG}38~{Nf>~k&a%w z*#`8~VKYvG2aD3aiLYP3&i0{+zeCx*{b#_eU|bOomn|-S4aDFJIC~o#laV~SPvH`z zN=rY8u>v7nIwQ)~9aubzoTLiX`ip-x%Kd+8@Ax~_OgZ7idl7v|H2^LkwJ9o=jn7=P zrhhALK`NP+*5W-BYe6MRg6jz~R7iM0U>naJsMd}%&7{2Xfgk|q=33Op`l(H zEadp}4vE^i@07~7J+_Z`rZ6N)2c)0m*0S>WZnC{~pACHg?;C#l$^OR3CTD(lwWqKK zl&`_e=d@mSZST0w)oq2^ZcjM29RwyFTz~zDx!vRoc1EIqbO{6Ye5hhwSoeo^(!(}i zVF;-54f_8dX4s8Cmz0)gc72Y)PZfB%hr32JHp?O0&@H#1A7j!nDkvcDuU zovI30iU~^!hkKORH{9CTj(skHc!4rV!rHl8TJ5It0A%3=L-S<}8oU0zjM?fX&M;N( z{rjm1Vs{qu^CBYpxaTnH7j|8p-6Ie(4319D?r@s`1d088IZAFmU(h&D^mH6uQ@AxH z$PiV#l{nNE!PjI-SntvELO$5)+1-JL z>kFJ_cs+BaC`PH@XrNIp0T1~xQAU)#7J@$*Gt zNwMet9c;$44dyu2Yhe-Y=3(#IrO0-7NE%%6@c6+P)B&9ohJZ+~&NHubC$VoqGzhey zkcO|b0P8D{*7p>BePSe(L_GIOpyXHB-6=x4cWBmbtHa!KlFn}7e$B>-*NV>6c!htlzs+psmjDicB<1?8ghYEPbbRKbes!sED)t5;2paV!K(s?jpJ>0AQ0^oIX>KG-eOsWa$o>@*1qZBtX1jJ^l_<2zGVS94w`XT=TO zW#Ag6=@@Xq;6%dlsHqYC(z9E9$}8?#&Z|m8VEHCQ={`uJzxaKOjqjxNhgMH4uXpV2 zb%&k}gF-&C#sD8m+OGwPu}+#-sj0!B$phh}LZ0=Q24juul<(La3^Vo;ZD&694yoR# zje)>Sl-J^uy7w%da6Bq8iEP5O%wVZwi*bB#zDHe1F-*DN{`ZUkp8#_7>+~8Dzba#Lf+9{y%M4FhRm9JGh~NTw4c?`tsC;IraZD&1{nE zd3k=%6r43* z80V>7ZbY&Ke0xg1@ZQk@C zJc8yWA|eVTgQ~-GoYAEk%Ofgv!Dliqjs;)<#SqFu@<2NPGzNyom8uCtSuOPH!A4q1 z$)Mh=SVbGxOq|a$bCiwG^yYNKAaj_4MzDMKqJ+l-j z-R9V#W^#gyv7@7SfY!j3!CBk!IQ9B~Nu)|w%ld_0n@`x7sLA{l##|%SM;(=rKntY- z)5DrmytcfNl$AAT8Sm(Ud>dnsXbAVFe*UbquM@jc2Dzr)LYMHoL#S?ENXqx|u~5$k zKotPow)o{ZiSe6=n5L%8cEhHAY1I zewUy(FrcDF<|W#FPmRU$Zp+BXvT;4AQtIj!7WQ{K9q8G+*UGMkW8C&eUw{DIhtB0` zKuX_)L`$nB&#$5Rd(=H&2il=oE-h|OaBNkxeHgV)f7wF^5>fpl9Jn~i$}hZx6*xZY zK5J$rX9k?}$0K%lIJERE@7;h!gEcR>XecMoJgGEQfS`FKO-mf?<0s6YZ(yrkLigGZ zN)cNC$KQd)ayQ~Wd8bP5FeM+Ie!h1Ty{9WQuh2KkPTOTYU6F`Dzrb78q9SCI<)9qovU-ZdhfrNDux>kR5#|ZabTtDL#@8~l8f-V zp^JkuB!j4a!C9!9`QqU+D0d&)&5^werfpT@4}v;?7lTH5?9w#k|VK-73Zwq{+%;cf@E1nQKT z2|`A?P0wq?^UQBy!4!trr(0Kq92XF02jOP{up1elS@fA^XS2j-xNpt@w|M;$**86X zAxLsfK4em#4ty=uOkBvajaT&qwa7KKj1J7s=2efiW0)yIt5%jTU5Qn0X8lG=wxN7 zXdda`gK8h4$E>X(6Mg<}2>2X7+@ILtefN$wRgGzLDBmae@X!MK`5BJvFF9(^S0McK z3@Q&Tp##vtP)x8?G2~@HGX1a0fQP7WHdFcVEgV$bhT?-&8 z1U$=N6+Ql#D;A6O7Z>lsCuYP;1*Nv7g%m63Sj|Z4{oHo5miKhl0YCK)G|7-q|DUG8 zZ~y_%D>|y17T;@}c5158VEhOOq1e}VpFBVHJsQc7z?Qd!H_-ZnhCkt-FQ<9|tO{5G z0;Aha$zzi~Ylk1lJeN$s^A=8Q!BiJ9s`s{LtC7NuE+SHU?b96a$k?Q(hNYAszhHu9 z8+Ui#HZ76ie$C4IBEa1X*Z9>i(i5d;11f|pVjG*qG0O-T*4XC}5M=Mg`SFoP=onOb zFd%vJ3cI@rX+5oPiYB?B5Bv=dF4t}1(>*WDMI&E0J#3ycd8?W!4rgNkH79YmZQyC0 zWNtdg!$A-kL^^5dX!H>AL%OCyE(hmqCUhJ>EtDtVd}m|=Ee;}r_LVu*0$i`M#Bg^%0R z*dV_lNt2Z+B~>geDtWhln}Yuhsy{eH^NF5J@xWJE$bg=Cw0v* zhD3)OhXdn5Vp6HJk}B@&l{fy$D%c1M#_{3XmMWZ)K@**_r%-&Mez=d1Uo6lHfuoOg zHYzI*u6gY;KZwZ&{TlpC&P6 z0w8~bwHHtR3aG`$ul+2NU@Y;$xA$IizCRmA)2(^z-AAc+pP`W=R1jR}=X}QYxzzOz zl^t+5Ie_~ln9vM9CB%-{$%BD6fQW_*rHY$->*c*WnoW`C?G8CX-P+o^K0A8{YFy;E z%2kLG5Ypg<<+q&ijI?hu1O!lMF$ z>Hp|JI)tg9RuKnMLm*+G{UP5tsLuJz`ZscvymiMwGIY@M6h(EXtmgnOeN^v1wJ`_F zyR9a7^L)Kq*1jka^_$08^9G~yJ~gb44m51dK7)9JzNbx*z3%c_q1}>9yv@*wM~w?; zeR@9*W+>WC68R=S-=$C}aCh(|`L^?A1Cp<>;xiOxE{?u8qd9@a zs3+g^FalC{Z$ZapLygVj&2Os)ul+VO)Vsk&+-|qG>SZkKLXT`1a=?jd7nTkW;|&96 z;iOYHV>jA5rn9k20kptwKreW>O5kkrvPS1E;B=YF3!fd!JgL}qvj)tZrS9KtZpje! z8eKh3L4|)K_$3^&Kl}N@(OFsBn!dZJAKM+-;M57z5}iM9o1yg)l*u0$5!YNhRsD+V z79nLsgk>y3qEP*zW{Vn3_8k?F5fE9iz3_Y7mKsj@qBIWB)pw@yPe(-2d7okuVbL+v z$KwBWz+p*ssC`_8TG38bm(2xW-9JtFxJPFd3ds;0tetIUm3P~YLi_+Uw*K$Kf-75EoUsHqlb+o9#0033pV}%o z-J>Q9!l>@ML-=zJ_ZBcw&~P9l9y~mpO=o;EUUwpt=)K1X3zwuf50ByAO>~>7gQ(CQ zm|!tb&SipI``8E2*0yeim(L2#E9Y~4rrcOe{(pIqnp4lz0gIGg@Kt~HC(pBog$I~h z?dZnXH$JL=jEH<;&*?3B4%+x_Sz?ALAM`%kCnju<$+`+&Uz1f)|LFH zs@V@pN-ErbzorRw+;jko*V|jWb`rc2hyZN4g(cDg+Oug&TJtC_)n^Bu)YSACFbude z;HqQ)uL);OD%)CtC;VStShF-^G47t80p_nyoc*&$xLw(M zA{vP(Ea2#c4Pye>#)GNZKnFJz9G4}pr=PFDB<7LL80z8Vl-EsJr>Ca?g7ssO$5zpC z8%Y&X@G36)t(RGTV~@!O^Y)8U@PUyM7NcH+PU!USO0N&f6F~3>gbLY=Qqv)L2SD~M zEUUH@)63~0M(9-azVqcMIbSQPF%swdL!UAgkz&5vDXvamX-(wEQ3id^0&4=Lwi$IvCO?Wm*%%z#7Bk4aOQ zC+XdsaAdz8DrIibKVc9P>>HG#%J`8p_K%MCABZ``flCh;8H5ObarXpPo`nTsI45u{ zA$|m<5L!-UrQ4H&Ds2UZ^APK?E*sRs%)@bWbd*(7Z`p9wRZ-}~bW)3nLMtlpbRADG zrdz4=UFf0)Q{gfEm6awHk(h8rI^+)_?+VGx#bwJBjc0JTadHuV)5@`4JY1o#hx|xJ zqF}pYZ>TaG3UVKt17hBv;hQ1Q1p-+;1gZpX-6QWrF7tF0N!Z$Q1B&}c0=uKy%?%ct zjW}nv5aA+wJ+1agD=Q2DAc~e53XPSH71*Eu#r$C~sQ2n>ys-qI6~>JT_(|oqma|E4 zCS0cowY5twE*q&hex6inp)06;rv1+aX851Vaus?!06cK)p~0)NX>NJg{Mo+*u{fjh zc%%httXo1|{Y?WR5)U4?L4JPx;bEq8c|sy>3vwT8H&TlTN^yKP z!6+%}cAAvj4Tl^jpf|%EzEV9s#YLw$&s9jiam4-s3Ux3|6c8x!Yh~j%aV_lN++;tS zf13Yyz{mLTm1B1fXdrwcSR#?LP5#_W=Bu|7|?Q4Q`72XjakbEov4=>J-wL*8U?)u z2asK1BaK$ud=bsHkE6!i=^{<|$UCS%zFow%P1OYU<2qAVWZ9^F?Ai zpdIGUf3EREh~jj0w}9S=fMU~047n<-Qwc=Eg)J~1aASt@_uso?w!yOxWQik7ba9Sh z128{iywS|ao1yO@6qit5V{;RZIOJuvW(NTJ6pV%dI@M+CUfKtZ6ISb>Sa6=>2e-&T zxJLOq6g1G6kwD`hunblp*Z`(fxC0(tz3hFHlaYFgF~PvkQf31biG<=*elT^%#6u-_re2|xTwLxAx8SU znk6=AjbiK0{GP?sVnp^lnuF3d|Hi07x=g8d)9mQMAlpv&N%{$|w1GYW2@Q5G60-0L%%$hrS zFCjr6A!`I-9es93@ER@=?LiX$^Oum#)na-o%Cw1US0b~XFWS3>aF~l+Hzs+?Yi=F- z7R=$~nC-O}DZuy+gnxxR=UwJ11-eUzAx1fz!i)`&hQez{mh(0W2~yrAeqPt_$}?>F zTf|rNeaiQ1=PY;vVu2NiipX=twyl&p?xWY@iG|hFUqZhk^5s{6Y=iZ1YL>N|^}gJH zx)NaOD|lbv0Q7u`#Wecg(@afa1WB}I>g;3%MzIhL)sxOQ#kT1^heIz?P+GQF@_^S2 z84vv-$^<2RkW*k(AakgTg-wd$mH^7_hw{%IvD3Dg!_5F+WJ}q7C57{LD802#2RwGb z&hS(&nGF1ROS)QGHKu=Fs8_9mXLSH*kCYhcO5GE|c(_e(O|PkqJkzzlmmKh)OKuy$J&;5TGw9tz23(Z^Srm0wMw-ER2>Wtxl{SfIytl{w2}P zXU7x0C&HfllhES-9Kuc<;oZTho!$Jip*X9GTi5Cf5IFkF!q!$h96V&OIbYVb+*Z*H z{p(<3dA0;lR8J3sFzrfwU*fB2lxd_o(3I`|ERIOpkF~w*hQJD5`eO*8||#jTXfpadTs)XW&;lT0#4a$!2oeQJ*EInpMX%b@n~`%rzK_B z#Ut}bN~I)+qi#x;@uTHstp>xZsVYx_;6y%rVZ|*j4iPq(u7D5%KzF#`i%09hy{zC* z6zBxa4fy^g!aq4i6PWE;p^ z1_p9~c47Yo^&}F3d+z?gfc@PFme;TRB5v4x@c`L6^MN8ktuD@&xF9ZWk|t|jyWfv` z)ARH|B#F_dg!wA4gxoh95&+Y_Z!5lbZqoyM8mi0vDl7S6CbGpUUk~S@%0XYUJ`gR3 zfj6P!HZNg{!r1tQQc~jV=JaH?L$RWg)X(`2V4ZX)i}}#iJolXPZmHNES1g=TQCgyv z17ThphHxX~$jT&e)YLygs_YTt$3`UI)@IX_&YtkU6_ul2N9c=z+eBDtTlWg*z&*HY zU2*;eN@C%a^O7^r_P*d<0eP7NnAg+>>CDtMhX2jXu8%X>(Uxrc$#dhxh%xN2pSE~n z)3z@~Pqz#t)LGv)S#ZV91nCw)jH)#2`elBsp&QT2Bk^5Mvf2W*WCC{}S}px^!`IZ@ zZkR38;bnUutIgRuK2?jK)ccs>*&MVCid$Fm1oJA)dTCD~?8Fdh@_XZS{Y)}?7aQq~ z@;=(r+d5n!d=zi9Lj5WRbEYRJ9iS1$1)~n7-R1h)D!l}{;T{_1zP>PS3=%9L2}s&L#-bO`Yy8E-%S{GU7oLL)j9-P zIwU`m@*heW_a?*}lvdEkc5!pwkb=QBmBZN$IH0Wa$?4PUEhtA}dFTm?Eg7fjG zKlvpFLbedl1SuxoA!PFEV5it05BJV5(f=q!X+_QHHg-bF@D^y?hd{4Dk%{QG?8&SZ zr2}XMBU|H2A!A`6=>8k5wSP97U4B*j;@2wV#R-@q_=^7Or*T(hN=jWFXw<*x)8g_r zMBSk!{@MErQ7*njdG%>b3ly{{lc~(bsyAvm<>pGU)m+HSck29k{Y=Gj z)l3dSK{*7m#c`K4ZyyS!pc5YtX<3K^{Bg2Ir;q*h)MwKK^O?Sr*w2skW|H){Mn3Aa zlQQ_FfVanb3eHf?bo4VQnXbs!m?XMn-MnAp<6U5RdyZ~_>Is5>*3}3Z(UDu(KOAmQ@g)5yJkD(-)>^)qfe5S-&%7YZlrw|&`H2PweKe@LGg>y!dN}@ zK2U-nHx| zY`K;-+!NhU5{+LF*7PDhwF%S&c;XPmMJaEYgV0C?ECq{a?4`1GkDh)Q~-a zc?@ZFbwGw#HxQJ47@^5OnDD4sKQr5?NuA?g73zKJ+~e-ykl0{&F%akS_1hFUmXfm3 ztEKlmB|TX?iyAwMSOn4T&PCQ@QwdFp$E)RK)Fpyr`U6*^heb!9Mn2$dKwN%>7tu5~ zAu=Fzc8yR%X3+0YGXrQIv`k1(!}1rbn)J0TyJlE z;O4FKTqNOU?prM?3=FtdSK~vyq9J+RP2O)gdnr0_hum+frr z6mJS#j`v|k?P}8BL9+vRFshgjI5l)YWY53?$3b>EPL$=syebeIV=u2*8AMIqqlrNi z2;STcBCe@P922fq&3vPgEp;7FHB%VV(|t#lxZ2hAA$1!2rw)Kl4K2a=m)}4~*@wil zE&9Df=@*Pr%Pa3%3K;Cq)~zUbg=omrqk+Wm`seaWfxCG^4L{;4*rp@#YJKgl5>NWWN&a-S$Va!ts!E<-)=(t zSQ!$gaMO#MWkj9&Y{ZRMmJWqY(KT&^=UbNNLhE!pn6^<+x>(_26UeOT#ln6cFiiDc zRlG%YH%9(_%G;s8!@M|CLHhZ*pHf*^?^&kZ6g3EulSG~kt!PSHVIL*|8yyahF=xGF zHd}5X_mhs*YxhbstfsiNbK};LeDFr<#NaoA*7U1?I)`dhF#~^Y=bjo*M?+F48tiA1V}hlg-{9NAd^G=fy_#%07UIs;R;3Y+e> zj&TRV$Jb#}9`bbb3Qs$gdZ_HpN$BZqLc-a(#pqxed||=$+$$yVWU&t^c}$BFodlnz z-JRSrJ(NLo2a=G;5W^mfC(;2{Ehp;xN!qMmhmbW>jSiLKT(LSTUsAtfzwDTGHf}0o zFE`CQMmZFQ%l~ui6`0E|)PnSS#Vv}>8HZT1tqoT3NJMn>49Y*d6)(7)K2#9r+`9ms za{{L;cAp`Qz``OSn}N@tus}8gi$hx1c%c%&UyoQz!3(1djk`og-E8tp1#9#FF@MOk zfJyza*~kdD1}c;?YluluNbh}$8tQ_U3OP}?sH9b|NGaG-2&C7lku=8^2QMAT%q!*LB(l{N7au3ubK z%`GsZ$9rtp7Utq4c&)fNBCfb>(o-E@2GAUlC_f~lDt(S)HK=`oGektVC@|2xeiW&j z_g?N^fvrI=Ls{#!ISLL};fr6CFakbaaCS}HDoPjhf5gJ*45FQLH+Si@^R{fz(Npa0 z`NI1nxA=t%7yQc~3~7vRZM`JFX-pdYQkRX}&rcst6m0Zibx+xGxl7v%TMz82S#`!f zC2MpZJ*UStgz@5xXUOja0nf}!m*aj9{4&znJ$U_kVa!L@oA&4WFun9eIL#oyYiqAU zok>AiZ5|qd!qytgU5p?eg|Vy&Yw3JcLCMG4Nd~??U%O4``lSPa{XHg?WYg7xsgCw{ z!^IssoM%LoBsif`G!P2W&69u>qHf;ro%T1oxrA9vf_f)g=jiFHE4>aK(3tN-Ok-m( z!{EkAQ35$E28L+ajF)$QVuB+S7VN6KVU!Yl-dKfX-ZSLu@p|>iK^-SI7T_$ z3XWbl?mTV33ysIAnDE`{9dULJiF-v{KS4STcgg8oOlMbssFPR#5mAm#ks4F`QIrPr zP42bPf!Amx468ew9t+FMZ@tsa@ThqQ^FHS)-0WWrtxBqVgpDAvW0I+Q)q;zQc10y> zG%AJ$yK#aE@200AK?X1v1Pmc1%yRUq6o@sKakInP`C#sH2Il!5keb-L^(GFrAW|4r zSvpjvF zd+_7%eag>m2i#I^^?2qo_0f>?%GdGAY-4ByQ&Z(jZBmD_3bizD3W9<*geR5loc8%8 z*OJ?2!?_Y|o`HcoOk{y=v{a^;9+wa)*3Ql=03+W(RxZOH+yQtYZ7#G~*X(>|8Jk4oo zZ@0gA>C&(_d#XxX{i~R0a-p=ypIr){M@KnkXV2lCE3>uD4OGVT=*{+?u69BU&9{@g z_n6s31cp2-lMmn#)BHYOAFWW)%Gv7FqQdo)HRyPU$5TDj^_iKs4bJ{rw6;d8Wa5D{ zHlN|M{OC!W&(Jw7vMX?&nwoOqsK9?SHKW(9nxjw9M|bGhj#gLcf)tjgyOqm03P26! zuoMLKzGini>y1Qrx;~^Fid+9FCpq`)9^LCl#JL(xL)oWs1vR&z89|T}e6Otc7#GGE z$Z8y0Jci0!Dv_bj*?m*IW|0<`=D}*c}N+3Ps ziGrd(FfG7vP|>W%heO(o(UCpx%DG|27$5=4N(XgPOg0jNb9tNk(W}^4<=h{sYFUHM zb4yNfaTK50bI56|dFdhBn5u!VTRCHF&nfjZJixfE3^bto3{hc*94&z)QQ+T7@_%WZ zEgUoo4ZV6xDI+#jx}2Niw`64f^oP_2s?zJ424~%0&a{}|k>MQ_1Y_Es>)Ki@9>TQv zu?nw8M=g(C*Y>99r3rT?9D8#rSmV35VNrPi1On+s?MCq)yNuYtONWIy_MCE=YVc6u zccVG?V?(y#aTph{k9-lou02w|0m6qLED&Yig2x^c&To%JjF>-c;_lUDxGn?FOLO0^ zcI`vg>{aEom@eR$7E30&FA(<%JH|q}Ls?-jC(b3pLH1jj(nFc9JJwapE>#<~Fo&9B z!gYa7Pus+W_s6t-nYSl1bhWfJJfFG=4zIN_;!L$fuUc5JVz{AS*-TGdxg}CMysclm znySU}j3s($)d`jgSGsh&>L}G(;(ItNt*rqhyZ=<_I$=iPc&PC4rZRdSmbiWFyz5b8 z+8Jcr|J}>}N#Ge1fE-5a=HVZt>Ch4jB)_Xk7!?u#0f&or`)AWA8tIsO@7eTZDrN8R z(qKI0+?h+7$Bg%mZu#ya#AG?lGu0$un-O$%h?~tAg}gqu5!5CZsphFyC-x4e9dt#3 zs`*_y;&-+Ta>Ok3Ej=tL8-rKd-#^{F5f07*f>@R$@S8oq0{nfuX)tUZ8L6!)o!*_O zEcTN@C^CLz0PA+b5mm8Kt28#Y%(qZWcH?)}N`Xp$VMZI+O$8|trvdzqQeov~r2-IKt3$TS(@zLqDwtf6 ziEKV_dlh3qo1x5@s*%m_RuB%B39pFbuEwR`-R7n6_@y6!Ut<#q3!zmIx9hUcaO8Uk z|2$fPds4+m%~+Or_$?&BoTsL0KyNcX5wLEY+(vMDE_#0h{h%4p`qP zaW}aAn-$|s9v13s_DJ2pAtNOvoxJN%Nu6K&m&vp^_T*?k|2ofAe=fkcZo9%-J7itr=l~OVZcM(O12Eer-FKla3~$uoUBP2VaZjqHtoUq^)cu1&!WCleRdy zdz*7n2ESqZLJQf<6zpZNhP!$Po;|SdwQ5XvVa|JfqEdk2z8}u&#Ar_#&6=(Gsf9D) zS3tz1azm7n+22$hmLy6!(n^PV3XPlismuAOHd+blOY6qhTr=FOHX3PU_!Sn0y!5K2NMQul1;=oIur>-l?S69@r7d4L^@W?dQa5u?|27Qj+uFV~ zx?2wo1VmryfZ}AEJdNxE?Q4iAcbLkoTKPe4Y}^UQAhrKHs1|Eh>lj#;qww#;Q?i12eT*6iJf7>?dChkfVVh0!bSvK z5GVnvt*t4Q*;u(tOURbv!h#nWL}>fB>=tyHVElt~nOqMCVXP|vFxY@a-X|pxvNgMg zvwPNywdZXFIB#g}@Kje#z(=N(cGrG;jk^R&5b5#+uA}I$%Mgc}7L12FIq|Dx<1g+?!SmnI zCehK!fA{EUVrPtshw#Ff!iXJz%WU|D?iTIqN4T<) z7rH#gbC~kpte#dMa*_wXj)>p|ydf`d54Y`mBjdI@G@EU9e0Z-^Y&^c>`ULfx+naq) z{FsAYBc(|1!C{~bwd_1aYIKUJF`WyTP1w@;M@=?cmGK^7uP0w@XAHaJ-l`-FJfFSV zuickEY)?A2@Y#1gqk{D^8?f66@rlW>_}1duF3c5vBiQqaFZ`ivGw6i9ZFAM=!{cw@ z%$u69a&zB-lSm#+8`G%(+rlC$?hvS9bV|#Ga6TtoLp5i45D@Zm^DlxJZ8y_A$HXaj z9q)%e6(DykSHFF^sK8+b|_Xw3VZsR9nm7;VA-W0t9sZ^0{Qy8@H&c zVHOzgH9VvE*1G_@5xeIX9zV(`=Hle+g|2#+IKr%6q5uty+_ zjt&q+-{{gz;C;f$8x4h%1ogY1=Ut^!yXMivW42&eW=;D^&d^=p zb-RgH|5X-QVtM&1K&4vq+MpmgX`hNz@x&2*MOodVMK+mo!>NMGp+6+7O9T_wa`$rh zd#E6W?yx*aiUW2PtSr#lDfEAHO!I=n8eQ;=R|k>AKJ668MfE z7(I0&0s``V&587ZCAa>aVL* zrWGO)u)QROkqeXoC#Nf~_FV42x!tCa9hRPjGxEy_);2t@WEp<@y%cMn=CM8hAY^Y; z#Bgx}n@&XK5#m_$o`;Nsd<;2+d;3>%8+TEdccXIpwh2m2FyR1xT!2!>3nW`3KD#dH z4Cie1&KHbMj2yACwg7qMxsREcSYE6C@#S_Bd;c#Z14nPT@0w=|Kgi612vEtR6KQ5D z4hq58I1LX*mn*i_+`3CLLt_Pk)WHBltE$lu>lNZ9n?}A}rmi=C*bR(xyW6%lXo?QV z(Hj_of>d~$xL^WiS=|85l@t%qI7j1rrEY|gy$$D? zDRCgn+3AMy`;=x7mVhke9*)RKepAc(I*?gj!r_v7JiBN5;2#S5u`^Ot7lbXJ`!Lc< zp6%@1)xBaPMe1m4sir*Lb4SQ><}z+cNzR(WXwc<1ue5VhR4rJbIc4x7xEvz%=g-{g$v%Dpl;?WTP1O~*F zz4f(}-0sJs1~VWW0e1^+Z+}Fi`RruZ+RAY=&-EH;l(*wq9i|&`-`qCj_@Rz#Xq+-~ z>_z&aQw#<>la(#~<8zc5L2l}^9LippMgtB2eT|MG3S=F9$3wINP%Kl15UErnyMhZJ zF2u{V)NgN#Kk1|*xLBaQ1_6NXSt_hvFxuZ&n<#D)!?K7Ga({AZ6BM)!#Zb%R}l z9NcGby|Pkay>jc8MT=qI%zhha!<4d-;l(g{{Gu(aY^+fEL;=bFX{GusJ!8JiJxUgm z?wwVgUsbQb&jGmiO_|YgH!EDt@+5kS%BG&G8V5u@7^lPn?YgN>=(<*-8f>SGcP0BBZu<}VnqqiM^l*1L8S?phPauI~bB0wa6=XeK2 zL8X&d`6;Wl%!BfSem)HN=!4zxIE`326YoufgyK&x?var51*$imYr$*CV}r*}Aq~^; z?T~D580DN?tNYD|&jwWTQFe>UVwR;rzy(2RU|y%A(t+0S_oxP5lZ495@uB;>cYF4` z%T`ouw!HCDsVX}Mv!0+gR5G(I0io-Pg(Z)TYD{-2LxbArrrJHHs(14EJ+pClsBUnr zxsZShecr)puX~>F&Fun5Ga-+fRi$eeqhMD`$=F}kdA{niz11a4^Gbkdx!tn9bum!4 zod%yGBK3E+OdX9me*>)FnoWg_6=)O_zP=k(N$!~l)y$taNy>`+^x4ineEc8wP)z>6 z3xu^G57%AK0(UUIxXNrXuoa>04#(^O_OxFtxol#RGWvDl~6A2DiHnO zQq79F8bc|*QBf<^IOiTxk^zY{E~SL+7gnvRtmG%VmyKV`ApBZV%z6{qKo?K19#>4e z%f)%)cO@pzemBdvX_pkQGAFqP{Td;QjpozQfqr|IE-^n2RtG%0xqqef+7&*~5a1UA z0#RVl%e}yVUFJh4NDRe*f~|R_ z0|!hg!~YWPNN<{ae%{g8**3dxFFc z7AKXWo@>1a-x>C7t&a4UWxfO<%zA3_om`rcCDZ#Ux@^y`c0zgjLobW&_qB{cKaRKg zx7^4~HGS^iI;*lq|zEY^Fs`E=zj0?fA1FlLMhL16$a~59Y;h9{7nJker@woV}JY zIf-@rKYd`TC^U+y|ND76_vdWt->>?Pmv^SPIuqXhc>zl<)6)3sgVK8&9t&I)<{`6` zQ5aZyhz$ewe+Q+YHBCO)`9feG+vp*$7+ogXmTL(j^?soLJ50esAY&xbOSXQTZ?YTi)=Tu@q&& zgt_>sWBk2!bU?#zJ2kM)P>1`}ucD$H z9KABtU|if`QQ5#IDIxZX<6#jItjKdMkKfwLhSiUC-S8~9DH!+GJZvE9*=~O-+HNMg zBaqT>-hEPTrP*c^`VE-F>c|}-!kagZ0R*k^T2H+{moG|-kewOQG}F>oGUQ!>n11p# z`d|Lyw`7evtIr%N?^fzwfvqdr-mXvQ;TsPz2EU7wPN)7xSzzBf)z|B?F-)DK=9;)8tBtuVP0zI)T z_FRDO3qfeFHmX|g*yG*ZQT{BZAikNpgoM1YHF)^5sc9n%Ys){cRS22=(`3Ai92{(L zT%zu091LyYChaK3K8WWkp>2?x8>tNxNKbkTb^5zd=p$|eSBNVbk zXL6dFboq6nI?VTA$LrCxUPV29rA=rsmWA8ECkW|i!F=Qpqifg*C%KxoZr-`sJ)v~v zXZL{@Cpme_`xJ>n#_bW~apg=kh@RV3Ds=To0eI4_D*4&fMOJ%u;=7PacjEjoj%s|Z z{&i}z(F>nFGqIljRIz}jX`}u1o^k#Aho3FXjErKH86}gIYuVmTX-OA957*9x6l4Ej z$u9x#!)>tN?!adz9gdu`>q%e?fJaW$I)*9KtWbTDi2RoxX?8 zCE7G*y|o>^XUA=abZ4{q1=<^7_?rI?(0XC1@11{Y#Knovm1O$GJkO@rpRvXCTWThm zP=+!zP9MZ2Dc=4dL3&SHo7z>k%UwuCWJy2&GUB?{(&>5kg(OfxA`QKcr7YgI8E1A+ zR69rO4P6+KA*cu%vi!X{jrG_x^Z&*sT+8?P{}$)4Gs}__B?z-=sZYq?z<1!|t6&!l zfGVXyMh6WJD45^l;L#jRa=^vW%Hs2nUuA2afJHa*4))s3w;tDpUZ|g+M*#Ba9Hu+OeLv~|ckNlA^teg&}X zReToAeEk0(PuCsKb>DtfDoIil*-1!3vO;K3D3U!wNM-Ml)shghLkLB7$etxDE7^Nx zuOut$cfLK(^Xv8ealh{8u6)0r_vdSguO_-bi~>y4U^H0mHY4uG*ryXYqH*L&%KBUs z8V1fI`*Jj4O~r&|0x%-1`@p{<_AToYQH!wvVYn0P^=7^X(baa{Rq@|HNf$D_TPBA9 zMxKM?wC=rXKS8oRvvit5X{rs&vuXL-MuD~T9qDVQsLc6^_dlWE#ZC3?bi~udgeTgi zT=Pr>z`P;Xs>fQKXI@v!bt{>}8@J$+9t@so&6%L@My~X#=Lm zSOVuW{QsPf2CjW$Vu64ZllKY;Ejou-?E)|WJ_?v{y5bkqTxdZiy7T z(@@emuPiGiU3+#($f)%}jxn0a_wf}n+-k$ENlAA5*{3q|X+|Rl#2PAntQUXH^)a}t zRvi4^JfyjhpG-tU)*m%LMmJa1uq_i^OC{{CV;cNQ zSW@R!13U2NVMAyc2^?c7r1rs&u3En_kX1411qMN!woLWsdc3vBr0Q7BWLmFP!NZ=q zV`Y`pc$z?@bM#J|&ej?J>)u3Zf?Id>%lO(C>Mf!E#psn0d*{{u zmD0NJ-vylV(p#LhXG`xZn>PqV+I=1A(iEdHWY@t3iu$jl6suWq@dPSmXIYu&xqC)V zvpL87=#Q0;6ddP6d%utPFqk1|&8MsOwHYJafDOYe{!MrUX;$$Y0)S9)d_Ub%TsD9y zPbNlvRL$n*y9C_+!Kp^DI2QSLa}|LAFAQkyE(g#F8I1nfSMuW;+ApD+vDK;zx@NeK z_~#23aos|3rJHEFFN#G=rKsTjeE#|E5MaovD#JsLj`_YI-i*KK2*1%h#G{r(Y(I0q z+O!>-^7`;|=Ws|fUhs7w0o>;IPgYdA9U3&E_NK4f+y4#}$tNC5Q@z2(*3_XTr&n}4 zd2w4X!ouNnrG7a0FX{J(0keuNch)@5oX0!5Tm08!1@qPJz2su`wLv^BeVao!&a`MM z%UO}_0gZb{`s(JN6=ygep)#Sht~ERg`q+zd1pInW$BldARSRx1fi>fGACz~B^tH#7 z(fTGFBHrO?u(_JSAsbv5((Uo$R#Mi~fY5+jN=-&Ii-_L;v;fT?YK&~`ZZDJ`++Cqll=0@7(}QP@_%!i+|`cK9&6+c3dvXC2)f z#=3-ro$$tKjEaQQ4dhjDE~fK7LrFME>fnR_AWDURmJRsnZ zZDGUJy5vh5CHmxwROr<~&WlC2*&JT~8v&LA_*6n7!8QJ{Pwh@Un`|z~c&jjWCGC?$ zmAJ0p3B45Nsr3Zn&PI7^k4xczTHt>efiv;dJd8mBJSn9jzRtP8?wV*HFYtyR3sj^9K}M9)P1n??PErN-Jvu1%U6=AV*mBMgNp3aRKBlYlT%?$oHT!bE)oNjQ zr&{`IjnRH^c()wZ8ycSz`1tDyk8r>B^$?7kQbnB=-XDeoW`EB4M|tPA6{w_uR11p)TWVkAg_b^y`^+tF zAAD>8&E4Oz)t=>em(Of=_#n=dAt^jsp?D?@v$|lF5%7dQL{>6Ig;4&pDFF`#va;@D zbX(k2)EU0!FPLlC?~3b^mFQfS^Kot4cb_eY;65&7DO^0$ z=Zu!%9$cf{{h*<_OU@+z=V;oHtd7$tEBZU#A1&uzbeRq*Z2xKMD%%zpU+>cGZr!)^ z@X#PO)C#m?(;5`j9^nVV5K{24t!ZL{@ABm|fxS3gzE~~gk2oGs(>7dz&K39-=srp2 zf(PR>xNk1S61XNxb~b`yF(SXUc?iYk^RRw!=+PdL;reg0@waN_6EQAg=^9ERJb0VG zfB4~^`uSql`G<@4cEZ;|5!r)MhTWUxKf>Q?t|((V*T_X+is(&D#{AaeIwg)4WIfQW z0Mr8ux3F$t7yo5?Xa#$-)xv!RI?h|^KlvB?Fc|BZoA!Dw zK{W-6CrDE-#VYqL89&H0;5SA#ieGp*FVM)oB2kldL!eym!>Ynxzq%(~*mo2QxB0P- zr)tjXl}X4mzbYQV17gohD)(gg%Kyf=z-Zq7jO(#l=IITcM!|52wIuna+)C)n4F_W*kS`a#EhwdLgUK zi?~m*m*>ZNl9jJ2pew3b575cDn4;3L)pEb}^w#Z?8}AQecNxd;$D~WzSDB0stQW6N z#%xgl$K@GZ%C#JDUEe|Cd3l2nW3jQow4EnpruCmo?)-oI8phbf>W2F37zlYWW}KNd zaukKbfx7m~+tj?Ox!=jj95I~VN_qj8ai~W0toa;W+uIHt|Ap~+C(3dY9Q1BGZ)F-#8Qd~?mAdXQ)KMTag@7%%=o(Y$BPXQ|b^ zhFJrQgpEzWG|QmipND54Uc#BOv9aDYV>~3MQ=LPuh~M!k4)a>P#{k^s|rA z(A?L_j6s`)4i~l>5Xy5w+cO?hRuz3A3ZRsm;?(}VLb!wZt3a%eQzj*ajJuv=*kGBffokgO?QgmItJ9I=nHPbFqo&K}3@+;Hd^-X$u zRqqpr$mF+q0k70HGXe@?&=aPq`hI?kwl@xwt#MZSk(FffQs2X zQ~hX|$>uQ$QU~gIMIQuG1=OU!zQE?q0wk-|7)%xU-@l|#74Gx6rCpp~{?qUg`an&y za9Jt!<6ApEv>9U?mD+JCsccBdZKPwsTP0-ydm|lKa4gG`ZFTA>>aS+nY_?c{DNxo> zzV(uTxg8ObVDM~pGVE>#7+8KhcJUGRxi(y$!Hh>WrOGt4hrL9H4u`{1__mGt(uy;j z@}s-DNcJ8KZJrcdx#!DF((9m0jZOu+2dpqwf=7&NONMFa;7awv+VH1b5mhed6Rr$h zsq~eIlfr)EUOas-FG;h|cU`IXEWgWXMP{pN4_b$xe`xzUMa>oWw6KlQ(K3lfoW%pa zyFVO_GyS=>f6wS@2DEy{0=^>+6e-kWA<>d0>`<%UMHHmcN%o>;eO9oC!#x zXUl|b*0npC6J0qeLHA-xZc#d|%uE`tFSiR+`WTyv(aX1$lzBIQ82KJ6-WU^e&*^+^ zUYTn`dsc6+=}2ehP|shTl1Lk(l*V-F;Wmhua3_0L@}WebAkw+TjE)jFG?;bHiXfXyH(mHAc{|;Wylp=-fQ*QU z_R?3k4r=@f8mSQSag&Ze3bLy*PB*7ve2j(l@cCjL_Od0dZ3vTYVQ#dL&KF)XyLtyR*#+_*@;i{&*(6@@@&NrKdh+QRrk{R(iZi71>J-{3w$UrXF{KB zZXO&R{kSlrZ$P5Pp=>=($i{9euwe|=<<)ydMIQcI--IO&8(*r*&Rb_>lNx_(?a$Uw zK-W-EK!Q$}NR|IKMgS5|CBPr(VT^M~@qarP1yxqkpkD>C_TavT1=?I;%eP@D8Cd4T z%2&fs#U?5gq)9jt?OdPL=ilUW5=C6M-fyWC6q3MpI^bF%8%>NvS$U`WqsKdV7#QRM z5CBrg6M|DUY8pM}A*eEO?U~UmrRJuX({JCN-SVmA!(QKv{YbKtHME%L#FI;1XE6z;sN^6P+yp!wglIch*g)*4qR zem)-nMQ%}aeA&ElhpFwJ!CviW=#)adUCzP~^zXo#C0tGTK4e*CPrx;D`H$!_RR@t#qT}_D28D3VqPl*?tpiUbzf_eXA^coz&aYfGX8-wX> zB<#J!y&7_@w=FE@$6#Q1LwtK}V610g>+kb9<$~GVOzW3ZW^{_aeY9i|0`a**+w1mq z=fKQADJK>B`|w2^5|vn^8et=3VtyIRNNvg0raEW-s-UbuiVSrM89C4@y(?bjN~Fz52UjZ;K7^b{B}iZp&FgD(%3^~3lH`vzxK6q zZ834GG<~4#1?w?H%#dfO9ev*KJsf6TQ2eFO@m#>QXWH`Gi(6VrLc4gQxL?VRVo0YU zqstgTV33W?A(v8NqinazS|}?La#Xm-U){^B)h{oXJclk2a)`W>M5sYgQEvJGc%zj^U&V6^xJ8T+cvk$ZWQ-7?!Ry$S|uj0 z`9sQi-lw0J#?N3zL1!5F)ozD8wtHb_VTtx_rB?tYLq5@Kwx*U-RQ99|@__hZ1(!J8blt;!Uy47%r? z2hwkN@yW?$kqrFMzZ&2C%SK{_b$i)@XR&YI4K8L^I76z3e*Hqo5EsHCGlf11`eOF{ z+Sz#+FU9)Ybb>k;ZtRl~yyelEoXHy*8BtHWk@B^SQzR<-b`j$s435{)b7mfh&x`cx z`^If~gW%DTge%pM`x*qbatr}XmZIyNKU-a2F=b-c?ob_^ThwgQO*(#`%QohNLR$Yi9QfC+XoeWd)77=1dRMo6^D1uS|7W}5)l8s60PGuw6NX& zVwb5j*g7M~Z`?OKwq^KfKaUnJoX5A?qTn9F9=$4z<^{_2uU9CF`XI|L5l$d~9^Mm;U(()>h@>fd*q#&jDT_CH~sJB0` zZH^F5{2W;DeS}UCY!)>)m*s3kjA`yi^H#?TbM3Hi(Gb^Z7s&;57{TX;X2=w zb4n4LSUVq2k;$LNy4xZM)qzYCo;BRCbfs?8+OKX~bwCsC01k}!$(qMFQ$fL{Al+x90v0LD(dAzG6ujB#%UAse;Z!ml9 zc^ZnP4G4f9OUAJYs>(^W509!d>;0`?4Qjg(n0T-_NZuVkarUZLMSvY?J8379tklmmq!-Zc<6 zN*;{{4P%}c4sGnPJ{iO}+gn(gxA;oBcTX`oAS^-RtWP>4?^5!+^v1! zV*5Ae7$WUSuf`uE^{`HB`1^ZQ3NsC(zsI#Jr5Q=J-|)@2jEjzkzah)A&~>PI&l-t{ z(uHrQgNW#bzvt=4n3&nUx;}R5Xk7>0C%ItRll&1&c}VlQn5fj!Hky8VuFw3>x(>hx zXkR;;> zsHO^o3$|IGf+g|8G!MlA83&NqAX=A&X93_H+ zIvoE_mJNm9i#|@FJ|?I8w`^j1Iu->}D{|LG3P`;HW8+c0YjMJQ)1IffY(F7o5q=|` zrixO5o*SZMFxYmxD{`RjrtpL_Q7f8SRo zpBRg88t}wFvx4_YwW^Vu6AS7Pn!%=da?ZZPZdhgSva?wGbGdV%lmNkth-hQ3V+jEN z26xU_euX}bnXYz%S2^a&B)g+&UH9A#+ut|UE30UE%lh&b(|=N&&DEYYcG zRet4CdIiV-dG;=d*{Bq+Ifx^0!*y?6Xg3)U0}lzxu?t&*We6ntUn*c7GA&bWq$`?c>6h1 zkVPRkvM{0B7DAo|wj*7rZLGj?VdvAQOLB5V+_xY4_7$xQTmNJ>%4P(sC7*aPzF>W! zx2VDQ7Vl|jYapD)>T)d={y46q7hs}<-cT^|P>GJS)&OjM@1ViMTQg_A(&JY2587zG z`X2@H+*wFP)VZM9CM*)}-s2&3Fhn+=+qfoRutAWQ^a&C;c&_@3e~I^JG_pFY;j`&c z@c@OTs040->wO0W#g*B%p^(ZOzya8yF~Mj1XXE+d#_ql+64@;TprnCR(eDvZYH z-Xq1`pfCXrCn7qEM{>mvC2YnN(%dVUGfKsIz}Bd9RbZ~QE_l*oHuNn-3R`h#ijs@m zas7h_w-Wxjrwb_n15Ci9u~FCi%jU;)&exGFB|9f4KMU7sCYZ2Mhet-f&&he(+)Nb6 z8rzl(V@M1q5LBWVp0_v6KYG6ONQms)_mzH4)F%$tzE#bL&O}Hbx&s1c@E~)cWd623 z6laRT2sRtu#nh>!7MvQ_`~mYydv4EBJ)}sLsZN&PXH?vuJdx;hIYx!~6&5hC zdSP(?!)UILEOhHsRCBKXgVvxiCFov*PGM(0RomBnKR5uG{!O20xN)C9&)xdnt)y41 z=l5Vyt70^;ju{XUI{Zi7`=ToD6mswRc6LyI;W=b{;+@Du>L=JLaW)q_Gy84 zx7IYXlklbmvSJ}ESeC`rHHFSu2#hkqhX(;G=*WNXn`I06ro z_wz_59{@9|lh2=P()ziSZv7>wMBKl&rDO}!_k5o$u5a*I!=Sf6UHg1>W0dn*+5_ky zO)^|3Zf-vh(D0Qecpuz3eu??j*Gc}gP6uVq$9VL$<38FYQ|wXPdi=(~dZe-7kP&;$ zT;+bMG72@noLnYcjZc8~QBt~iMG<26g4l7>uQR64kiKYPp=5TnALt;&Q|P_GJ`XH! z6%*DVWN>kzQ(gQaH-Z@!7x~X`MdcvTvn`}EAJ)$-NBSwX%f^UAU9MTr zTuoMKTMf0VHNK`VchP!IaB7Jm}o5`#}zdNy2Rq4lI=D^kcZ&$+7 z;(vW0GrsJb98P`Bh6M)f`lONU*e67D5GGZuGfii1l-uKrV*_*?t;1!9JWp9E?UTfs zX?yVeG-nT-{XU%&*JjLd6gQ*Ompd9{*V$~Y1&US7`Q{5jwuQQnBW64#1Ci2j=~C z9d34;SoI?$rL*(LlHFtQRD`t%)9yQ`NOm&%Fa32e^#<|>NXN6{(~vbSnPydWSgp}r z&)z7C{=|e!fLiwKiNH9lU0Qq%V;>~1OPavsN?-SopaX#<1&g_8)v*QQC3_Nj_^%l9 zQrgPn{mnC^q<3G$unA}nDlFuY6b~&?N_kPTSMA%Q1a<>v89GZ05lvzF)|E|AeiA=T znzp$-+7%mnlQgfV;@2-;xbiB%d*F0^!wq;_TUAvx{wip4jqqgDEMxttlc>yz&0*u+ zB3Ug8m*N#65(8@4y4)q|boL|YnAwP#I0i=ZK1p>f-vPNa9Ri-QDa{Eo;i1c zAug}uvRYV?J=~V2CuiGQdZv#DooXe;Yl=Otgrg^yAEtp}yx|ja)F&Emr^xO56NcZ7 z1+omTitgH_gFglZ0$W_1M-E+lwV$Ndye>gr)8K5u0$&Wz*|UeA-WUjYQ!umcxfy(A-{H}F7izQ4Xtf@nW&cYrQ5Wb!Z_DNL$N5K)p-&)(#$*0d zw}MG1t}};z-AVVjvT;tnG1I0owx3Dhc2PE^y}kWW))#*phjw)KDcx4Nt>Rng>b^po z$Fr3m($=Q3^;1E-8sJ$|?uM9aDG^{;8$#&bw|f%no<+2kDv12_X`*)g z#!Fu*BB>fNx8k6dMCZAOU3qv*KI@;wnvIx&LCR0-_o8E0z2hVe;%_s!TRiB}C>^nH zm6L=^P*M^FfNS{wLv7TV>35q)DCiWFh*t14y8aLl@*cFF)j1}zignHLbXO}uN#|SN zeOyogCIfTeO57{1I_*OK3h+G8kc{{rOt*;`TYR7MB=%Rgz-4TG%GqQGA+C!v1c~A{ z8;Z*^xUXCSNj zc@cLW_-$8Jslt;Jw)69K&i4twHU?wQ6AAG1^SeocW5>$pwfCg94nqCei`Pfy_Ib8| z&T_C zKF}`|-!ng!(IFjs^Ekb#>_wm!@LQJKJLpR%2lHEUMzR^FT}d3?`P!-?q9uUdk?8<8 z5bg9&p=Im=Xv89YSiEJA`!JJCty+K+14VTy_L5rI-W0ogbHmWV48#_#E2g&l_Bfwb zO^HfMB1Dw7ylyC>Jc894YHsbHOj|?lcLXC>eacoT#iHdSBI%Hjf@RSAkKh<>=c4uxieMiTD z{nSS7!$+uMH$U%nhIijtk-{w1;p0{tj?{>P#10tZ)|HbLxt{1`RGSOA4vd`ry(Z?q z;^1D5y(Ao(=HA}o$!S9k9iJODN!427x|b-Z@*E7}fBZGi4*x!V63c>`r#x?)ed<4L zs9|KxS-ku-G%8ncWyMv(m~gg=FWlex_WASYRR=lZI074v;$nT>@SMSa^1;l35ZRD| z0*;3zXu!jezRa!mb$+1p=EJ-R+hNUSpJDr=oK2;Su=}RAh{QvT_V2V9}Jdqx+`w2Rby?%M#I}@<>k?#bMGKB%TBQT< zcD{!p&x;`~vTH`q_7BTJaYtjd9@Qk}wbsNfI`^&dn4zDwlo+bS8svCfqY+-ilbwC& zWmTHLR}q#gntNwh8Jp4th!bJ3uVutCXGcC*Q$spxz5C)#ziiXo8mtp+M_2in2teg@ zx!PL6-+wRDOl*8bN@xs5ZbV)HtWIEKhyCQEA#06aYj2VzH8@o) zMdrl{UCUO#P?y$VZg8)jpZ_v_;8)cAn;~q}D>x2&Rr5wRn8fKyHOTr_XTzkA_~P23 znrUnf_fu_}_xXi{K&k10^A(*}mXXS;p&oV{=^?NrdG0D5UP3 zx!&(rZJ^~~qv0^o`{1kHX=H=IyU?sb>O=1z9d&4()}5W8dUJW!i!urdC{}DH z^)2_ya;xoGw%>3Ny`l)!8TKBZLCne>1_ZX)UWo@u6>E(S(r}${8QSmuXkg$oO#6Ij z$b_znwoDS2ZB3U|1SyD@V~cd#w!*n4i)^O%ac^sD)zY+XU)wT1F?iOOrn&Hnxs=!v z&|nn6%HZq!&7T>@P-=Dku$XJA4eBF1$dgqV@w2z!lpn)%r|q+@(X6-*Ika`(M!#JM zV}tJ@{EqSRW6Ny0YuC!mvY+%hI5=r)(ozt^{N~WjsuZ5v5jeMI5osEUna$6u4NlUk zXeJaD&ZR#yynFJq$Nq0M3r`O&!!rjy7SFr7II_4-$RPSwe9xYZ*A3sC;0Hp8fy0*# zR8g`V>*b&#Zl=_d?0kJ44_j}b^eEPVp(VrYQBrclqUiFLuTcMB(ZGeKjLmNMn@rQQ z49v6#76D(Ih%Y5=hrwI3AoZo=^irUvsZC;g|HDJqN@fgrWLSeYZa%#jS5_uNxsCgh zzyI$23-kXuZSWaHN*k_{D=aIUI#CV>pg_3K|Fly7rd0A@iJ;nz=wd>3$yPc=1l6J0 zdZFvOCVcjGpj?=YXdQ2Sa}3@~DhvfBQH3;}$b9%SY3|1~k7w$OG*0rw0yo$2?0f9_ zZmu#74B46oNS^SKy5t%1Nnx0M!kk)1NG``nwm1>LiOv%8d5zmQuv*4X&3&Dcj4i3n za4>;r!d^?oQ;jd2o#wjPTItf+O6vkyq?}NG3prGiw%lMIz#4##=fgk|-mIiH<7u6s zx>M_OvxkW+N-eywv7BizEA~q=v0I$W8wT3-P^@S`d-}|i$!$Pn;b6JB#h~l_x)b;9 zh@U_tiw@MOekFCPp1zf)+MbiszfU7n<~=EN8@xQFjxas0IpOc}B4&F{_h(SdM&5>x z`@%jy`ZYxrezf|==G{8SWmr59<~8^AU4v5ki^l_1)x&f;YE3&*^PC3u+g(l7;X&I9 zA%kl&vpr90D(eooF4xyzf#Vp&PIk`Z^dzRIx%NFNAACEk!*EQaBxS{~I-3yKk;Qe6 zzxB(MabSk4+3WCdl0a5o-of$Bd5Nz9CuXN|2`R0vc5h5`k=7ifl=#rHfNDV?peAXg z%q~+lBMiD2RXHc~UUhI~1AX#^SH%~@B}O5|XQyLaaio1wbAyILLo2M?x<`gP& z{g^(9GbpxU$>d4C8#)sVw&^;zf6jF6gBQtHeedi#7-L($ZaY-j+#F&yP?S`QEWOOE zKp6g^pFn2J>uBg6JGa;6iQHop?ta0T_wlWA`B z&m?29j%b%D$T$VvfzM`P3pFR27T^nC)?uc=AQLF`B1-G5_!gVLgxk20ss@79Ld5cU z+!k0+o{29Gc4m^pC^2R*WBaeW)nNI#jS=?^i;wE2+NENNg}+$>oGAwcFH;czUEib^ zrjQIBO3OQnnZ~S>X=!X|_SvLp?57cVcm$$Rd?VJC!!AXJ%S8S9+QOG1UM%D7e4Aez z7J7KXmc^62uRtRop$E+$U^$xfWg`mwJicVv_qQsCc)^)|n=vg(D-6dzIt5PY9lk!U zTi%sf;I}Tu-o#_C@S=oo*$a90JK4u-4ZT~9e?MQ8p$Z9I2o2qnBjuRaINAqSZr(^C zp5y23XIM|4Rx*i;IS{|l7^lmi5$aXRH(~3nwT15%`^vGJY0WJy9|}wUotFOF|LdoO z;n)o!{+;8}IfdDDmcD)9_JI7uv&!y9e%yx__Y1Tq(hg|jUxj{%5| z`t{mrN~t?NBg#~}BO>15OKhkOy3H?e9)QS8|BUp}PC$|`7u#-Mb2v;SrHIgO#gtk& zn@vnBWp%N!=h>_F@3&(Q)mCP(ZhEwZq z9QGIc9q*v+$4s*7()UKx!L7okNo?Ga6>`s_C?R@%03lVFB3mz=`19dvFggZHLeY&r7Ft++7hRCyV~VlRPJl z^4ru-^)%HU{vbsP;+;E$Zr}C=N-2F+&7MoO|8$$NlTs`_^Wl9XEA!l0CxT7^8$w^| ze01%29bZt;aX82~G&J7u@gY(w3VPNveNIWBnD}?ZJ4Z8GOI)dvc4lxufIj<%6g6-@*bh z6u{J3vDExUX=&9L(s@UBsi_?~{;vO3u~KaJU>Y8-l$&Pskv{h+k7@MBdy9YubDoE{OE=6wrf9G}h}Tm> zII$5TXVJXE&V3p>=|fUTsViLbiV==2JU#6w_KoIUPukB*-?9SF9wVRccg>>|W28$| zSkcR*QP=dgSr|^8$|||A_m_Er^<6kYUP4$VmFx+bAJH!^vv|=b#yd0({Hr244JvBzmRT0;Hp~d1Q9y_&rf$CneZ|YwZh{AQ~Vuz6$k}FLcWF$_v$d$@uj7eXczwZo6r<>VcpE_=T$dD zwYv18w=32g4wz-QLMtcvTbeWNn*A|AVSs+8@oU*Nw>?*nUFobcv6h9XHOn! zukE{;QJRkpMuafv0`0p-V{;O^NI1c{!@AHli;RPcRIMxsP`^v+5p~y~t+IT~h&}xJ zj=Y{iRTk!tg%QeE?WVaS3u)Aol%cHx7D-DRylImsuM#AaA-McdOc1F0p3=GG?y7;t!DCW1KOK@mG7=Y?kaH>cLbiu3I|de}ddo!?nr7qw_Y?iYSaYe*KWE;1^^ZwTW4$ zA8#s4F2zp4u>aPCgA?Kui&MzzMeH8;8Vi{J-OQ!~2kGNu^-?;D_BAjT@Oq5=tV|H*DDV#}5`>>xFLahqV9*|F16GYz$_y^5Z zTj~W+p`mMPa2-w)w3;(gE@5m2r$zP$vKT0eYhJ~Q4-3zXiy{#HZ|J7r(B3~dmonJ&;)kJjZfW7HS zf@TbM3I`5wvjnIV>ufWG2}npZBCf&U5no&q6_+yd3UG_u8c{d~BS1(fs?C#E ze+&|}YCpSQ-6l#CwugOuzGb>fU>6kaE_Fxxnatu;de5u=_JfP(F~L!gCBQ*x_Pz#I z4cnsFWDcX=BYa~{3`{bd-7o=nOCayZjkI zxI*S!*@el$gEd(%<=KDNpId)$zq7l5t&R@}BQ8=F@15-Owq{eG_KecPu@)`b32RYkS5FbS=et53K6)pR%al&v^#ACmcTho5#e zM0f2ZIlb`5l+o&uE&aC~ejH@L>yMkv#-<`y=qFa*L0usVz^I4rc>54<%a0%BzpaslRuk&?Itmcf zpFgV@mU)E|NggjQ+8zHcg#(s*OmE37*?-Y14rwoyv$@IodK|&GDkX1z zgpZxwa4dn#A4AOchLnCLJ$_h>=lEJ5Sgd?V|11KrYWvt2dI$h~Ko&s< zVoGn5Dx$8*s-K!-Jt(nJM;E=g9tmWS%4fH2VTsv7uXh@8dD5wqYQGD{#bJ@MVS%-y z?fj9fs_KC+_ct_vi{cdmyM;Lj(iC88=lBlmTmG^V|J&R`ywe_WZ>3i+U*3ha&iNg| z#knwm6xWSY4N_Sc-B>$zlsbguDZ(fe#Lj;3Yqh|vr&h;Iki?e-DRU}Ze(F&dHjd{g z92FUMcIHSN2dtD3p$mcb3m_;HGs@DZ93pnnc)bgMeUnz*r;-zCZOoIO__#}{zg&k8 zHkB;iT7G_Y!od8`IJ%iKvo0|#VjZ( z9OzTrpkvg5{oE6IcB?7nJlbO)k1cWC$m>CfMt9t??+nZytq=71j1=5TLcp$U8e0oIiZ<^PuBO-> z|3zQsX+%Vj%DHmy(`4WE_qQO&xx;&{r$6{h1AV!&vBg!2X(ug8%FhgWY38u3A$g2j zO47ZIsYC~va8(XvqK)XxjLmyzaT5Vpzn+jkB%I>&7&}^oIR&vEwj$VbP}fkbdPceL zZTlePuz13!@=l{$L(8-DYjhcb5hw+s2@gx~0_h^wX?!UtzldY7Ye zO7(;PrMZ(v*$?FQ-d9rEf9{-*^H#K$BDIM{No_`(aeX7_&cVwE8NSR4W`E+(p0#d_ z-W1tOPS1`sav^F2&Sspi?nX8nOdy4bq=L^|#rY(#9kh4e)t1bc9xgko5|+h!>P!Np z2c^G5PGidq4K8rAN8{0=Stk}Uxv#%sRcy%Z*^l@q83ZxjH?~Hw0!!FwtFiiVr_Ckm zwPq&QU_Ww+vOjWV^$^khxXmL zJeofgjZELwK(WM=0v0M(-WSXx6G%{w;RtJ|69$3gxA}DGy1K&vC~x3sG&CH5J>Nmo zn&e_NU@usq4y(&bJ7Ld`cW~A5)uq_$CMnZ?1Hkt2Yo3vR?>`_|lo*Bt)dnYM8z@NM z-#6}dv6UTZq$1so)C%$H(K-u#!APgSlT7am3h2tphWvzd?Tda~inu=JAoe$p5;OsV z>QvQ~>YL`H3~nR_P0X3UL!b_n4H)I22nY?;$AyDDp|sPf6vR7VYW5de~op7z*-3yfR>5prpBK5YG_P_Dai7{btfVlj)E9l`%mI)%GOScPIt+gxXr#tS z4)Dsysl&5Kds-dYYm)p2rk}Du1tVyJU`e#G_JGmpokdHNe#Qfo z57YL=Dmoj;zH>AYpksF4TboN&*xij*<}lzGxEoMB-27|3KWeVOqo_1y(`_$LiLGy4 zSXl$m!+Ir!5;fO`%Lgx1S!^$tjjt|18rxI!5@(4KHf^QUKl30W}pxP-|KJoRpkmrOU89{)DAA7sGFQ21DUU3evVlecY%$K zA^~~H%kz5vJgR6KQ5pA|lEkF^NUnwgwQ;~TX;00!r9?Z;2=iandvsdq65(@*!= za724zr#I z=TrANu-n=(jCoL()`Rk-oMw!PpuI|U;96jGUccsGcaD^ZNHIWetU|Axe73!OXwWzZ zCcT_*g*_oz(Bu6z?!I-b^p^+E?>zT)2@2v9araq|`oLLaHAi=y`cp3X$E-jQAks>9 zB6sP}2GoE%Ck7z{e}>j1bKG_?=mN}xK9{*pmM(?4T+8B&<yQr#VM4lWf#CZNwns)V+~iV$CFy zJEfo1Wuz+Wp-#7{H26m-5o!(nu-u)j0;^1|$yu{*>k`1i<4p>O{Kt{|{YJbmJD&cB zY3?%)(kT-&LCQD|Xh-L|8`{xw|4U6Xba4r3l%EehmXU|pH^MauBXRiO0ECz{5PN zk*Wh*zj?}PGB&=Isad?7v1f-=zbUJ*@GD$}G6?QUBcj~ZLmyws95+ z!6|vIo_=b2$O=(Was`D@3V{nIU+ehs0}=KLb`S3IN#*j9w(Z|*esCd-b>Bq_xXpVY zK$a^Mr;5@Svb~onzQG82u9B7_OWl7U&wsl;DUWX4`lLXU0tK-)H#|gDTwSma->V&r zD(LIGY{&8i?aO7)?zy$+>Y;b>_&v!ldkX4@D#K;^_v+%p_gz-6Z5LH*1W_8DT`wn13I7%Kw8DO?nqd}+vod=vBsp%Yp z8=`m1gVMx}`XbQ&)?i9`%3PjaPpH8q4c^rAAv8$!UD-j9)N-r+TwU-x(BWM|yF*9) zg(9~aE^zd7=w8*iB1O%2J+_CNgby8K*jH+gs3h!?wd};r5&eX>2@I%b7W5hdrI{DscM?HsGR&`Guxdbnkx)( z=27Y{E8fjMz~=1qs`O@$A5E8@7iN`t7hfUAJZOXy0;Uk3_Lu1%lmBS}a5>^_C;8G~ zTF`@$)nqm+^nhQ8R(1n^TV}JoK|CSL7=kEzc}g!sb@dmp0jh?-K8G?WX6^Kb>P4{L z2pGc?ed(KKO5>laPDVzyiqvJOAU|)o1pib2LKcCg3*fD(>1+?o%{!OGgYJgosIC>W z1a3}cY@OETehe(xqIP^g{dsHe@#uAqCvV5S*hOGXyH7uJrdu!+^fb)ZD@N-e;69aJ z?%Xh*yx-n#MHgJUn>ak}Doxu{^t)yO;yr)W3dLS*hOarT<4opbyWx=%W|E!edA=B@SOS!M z^s_YX_9#S6jkT$TDBdXQ`*gyyL+5+v{i_*a$QAr)EtPY;b0pJh@(1D1LW6}Im6m*> z_Y6+s_;JT8-dJKuJt;@h~!wpf!@oX8T@VNx-hi}hOQFij&fa;Q-Ok*_9MUO61cLdAwO3WNiujsluwHcdLxCO8tifMY>G=8&EvhtXtO{$F%+a?&|oFVsOF62+<0uMm<(cRq(mCD#yT&uAFzBQ10LIZCQI5%74Wh;sX{*Uc7vngV! z`#5*Q22ATmh!E6e#Tmj0?Er4r%LkVrFB68Cq*0#d#tm{dwkI%~!oC7nU4TwM2Idp9 zTZ&GzcczV$0UHygJ07Gc?by?Ep@%UN*$VwSOeh-vaLUh<{5GqWJrT@z;9QEFb*P3x z@W5eeH|C++DiLo(^vO>;km*@|Z^B>Nx5zOv>nP?;MB{7p%h433%{YY^RG^e~b+U|iID*H1yg&9vem_1Z9^}A$ul4m@@twt8OXkJtsY*a|N1b}7UmP%vG#>NT8 zb-SNEll&R`+^5|g!Nnt{D-bV!8kidDZ&#d zMIvq*f*piNGgH$yGSv85U{-ogMC#UhRYxz?u5;l#2Ue@>V2dBe0sAkc;t{}dXJ3Yt zwwm%sG1R)nvZ#ZHWh{EWoFaKrxZ&YJlM`_rhUxV3i{DPi<6T8QrUTcB4@v~2k|Yod zk;3(mHsitOxpTqB0m7D+2X{aV0sK8E(OXBDrZ-&3P^f|w=1xSO;X8lKQ2){)D&Y3R zzehC+3yc3Ot<8T^d6+^tCE!ngE_$CxjjzZa9cVM=1MWgkeh%BN8miiAK*(-#svv%3 zYW&aNX(fo&E?<5M8PXv|G}>70h9u+)1u^jsfZqt)PQO6CO(e#6HwOIC-}NJ()ejK? z(su?fge^{-eXF@%e$4$J*=GwK@alamego~eAN_-I{8Z>A&{lSt4%P&u*|1N`Yn*i8 z$!u&>+t#*_N~BzHOEJrOXffi@A9#Hox>Jx7Q@LwM+%s z*IZz*|21Q4s_gEAQ?(NloRgneHVU+lLRKwU(vFLKv@Vb5*3B*e{F`ruedz*?2PEkk_bf6j`kZd2wd3^Tuf>=RA8C5> z?1y6g-UIj4MGj~ zC@oE^yO?|Fy!*2=yA3@nE{Ub*A9pS7^Qi~i*)>TB0{%z=$yorpt3cM@KZBk^hdckU z%i%H*!ti=z4m!A|w`Me89XvW(I_(dk$GdEx^}jlI?W(8ebO(Z${Q*AeRjoZCiSKKh9}$f?W1Z z!>qNiMsI6tdf}E9mntp~m|+761Vst{fls&-ZuVHoW^9aBv7Q1}U}h#;s) zaan)^{imINE*EAr3DgKWzxOS9+(fD=S(?NxG2bEto!u6t@h1kt(HRf|GdkWI}apZVk zrUAo?ILRDMgS#gyCMHNdE_py~0NV4af8hi3{{GuyqA4$2<2ZgnA%w{ba1?B*N2?Qo zbbmD!geq-ls% z%a+NX<&Sr**tV=-q&-BS|9sBf(-{u98htU4%0Wepar9iwn!B*uMu{QcNm|;ujinDQ zGLZHN%$sFcltx5G3DdI;FQ8ukH9IZ%!5}-F#N!9PIy|&bpZIyjOJzCVlN=jwywTEm zKQXa7PyS8eDJ0!-_Pk7^ePnTDSKTNU0wdO(?!JM9075B55}+mYYr0ph-=#^$MfGE& zaehNIwf!ig%P;n`;pXWUyuwY`^mLCeDG7;LFL1wvvn5$x5}J~8sX$vLZ+k2=56+1N zk@8}BJ^#nmdxvA)_y6N6?NnxEH0+S1?4)I8@2xU3BO|*~$qt=_kR+Rsol#bt3CYR| zAv*X#LwO#enRRAxi^4LLP@DT;BEB)knkYt8ki z7M3L^8M~+8O_=)8oFGWj`-kiV?*J&h0YD5Ea1>!OF~vF6=?dyqUsv`QK^XYnz5d}D zWuA$%Xk3Ix{#a(_pRbN$KU=>tXfuWVOxWmE{8FbzpmdJ*4ctLISFj&-u(bSpB4>DClOlHiHqI=`k(9|6J>p@kxk&rb z&zf$qpY>79*BwSCju5FRS+1C8ZX~9Plmc}?S5FURp{2TSj_*3ZHtFtHqq*EOdVxsb z9tB1#78ueG)tmd#fFwUm!DVITOvAneA}zYT1Y>SB-Dx5Rs`eD5;nSwbzQrctNNxyg za9QJn&|syKypKT~kQsstfZ)N3+GSm7Er{zJI-Yaqp1U}zxxsXRT6yC-(Sn{)7?cZR zENi_t!qn# zBv?2}mA0_E0rDb{CmK7$c(?}#p?rPv>PhP@WUd7%a-cK~QQi2}wjEu){=<-R;aY_3 z7@sMyQ|PXN3r+OAbV{%Rr!;O!72lM@G4u(aCW6iy%y^h*nth{?81y@2+F}f#$^M*4{t4~ zUu-!L7PLT_@Uxy5V{pxIE>(!REbAOP4iqH2@I`ZH2SQ^Y>`~-dC7fOV8Yx|{KQq%2 zU$y#8T>V&@#Cx4Ss6?#9Ei&>DDOtHOviy%}`D)vt<;6myH>IU}vpY(s`g>Om@mb;G zuGTfSrAFccS$2maf0PmNa7J_U_2O)1QmpD>c_^=EbjdQ>ECU14B=8J|4co3ing1|I zd$H(k|7}0m?jR-TZxjA0=GVU*)Nf+(i&bJlO}ecPcQG16y>ZMaNlVvdMR4!hib|DZ zBX!~=Hlrf0FS7DcO#4(?KLSHA0^=sD{ z+rissB(hCJ+70e6d<_;GX6XBpDRS>6&3*h3XcJ~&%*BPj|ERU;-Mfh0Pj(&jyU!GG zZ=q?!?4)m)n3hXwum5q*VaA0D_dhLL7=vS&lX-~X>~Y=>V2Q$xqwZo>wtd-lXN9Rz>e4TGQXFnyDn~5xjSDdKM{S?GN#O2Ug;q9pcC&o{LmK2OOpuckL z`fJa>2Qa`JDcQS-0lzeP!svpLRK&1NuQ%DD(}shj!lh;D&0mqTcWLTO_@jP+I%oxS z7&1SZH`BW~gBBT>tDK-Fy*FqLTNM%t@WJ7S!}^L!76g#skC__J>)P!!I^RpPanIpA z3c5OKKTnM>X#O|Ca#-yDCy{mGd~h7cBl2TmMHHZ(@V#%sUmscVR|C>8mWiW>Bt!4t zUnd>%+aVVG6ZbyyUy{6HCi|ULN4V>5U}DM9UJOnc2P%i@Up;N|`+6?Z0z>ORU%`qE z(*m93{nXUAx_kJ=ncnCU#>Qr$LqT z{4(#3`TnDq4i>pNg_`hZkA$tPE9&YIwm!DhU?*@L3>gK}7(x#(Mzk%%ROO{PmkvmAgZw2J0?KKNw3aF z*37?y0n65(>R2g&b4`k)o*(ksH^&x}9yVr9^A&ZN^h+7TrG63O-D(o6)APH4fIH=%&)5Y zOhdep=@>rYsk(nJ277qT%_BaSE=SM~WT4FGr%bnKOUi}8vD?pgKl+N$;fCRHT|e2U zP?eW^Ho7cq3+5q0Y;&BL`~7gn&mH3c#eRkly5cDQF>ct@y$->%{$ukbbt3wv$FsM`)7+2|DDuZ{-^p2u0>L?I1>o^uiX=C z3Af1JJB}d>PR7M$^YJJT9e4l6q7YcHrQ^f~h3+!zLMXp+^CaydK`xVj(2RjzQ1ruE z*!7nzC-Az#IsomUr+omPEjqfJtW*u1R%NDtyb@deV5LI_z_N!k>dm38X?ui+@kH|g z9|XpD`Vh?fs%28QD#@$WQ&=K9(4wHw-C6%NB;rsY$l`DZG1=D3D$BgzHwr349QD`$ zHYS@kIAUXy&&0Tlyr7Z9taRqW-vJKnVK@Q@W;<~K!)1tP0xBLTdN8oY#l>v>0U`lb z3~%r7@pEVC;XswdukH7h_0&H|4o4#3REu-hVD>n8d&=TmyUABjL->ZH@LLj;4T&<=>u9d-&O#?`>@~IQ&?ozkd_7#;XcX1+bi&n{a|K8By;uJ?il`GC7XE z+ETzq`w)pKu^9QD&&EZd2YvCX!XIYl?KEHmJGz#T6GqofjboHBaM&i1N&#!G>u zJa#qZN8zx6hP^H-6m<7g3-`f4#I+U6`?A#!I%r7c&+_Mf{QVnPkBX*NINt6#S5qT_ zbJ`Y`st?CDydMtiCM!6!Aj_!IEF;yB~U@6Zm2=`d)_?N02KdSZ6xU|=?X@yYekXPSnul3Om z-QqWyAHq4{?M?K&!|~4?`1kj#-SaHJXS~UuCDPv&U~k_hur6+zhsidz|_Tq5A*+i;D;0%ayH1)A`}HAcd1L+h1Z{d z@NyTkKj$-*IuASAt#x%;2pF+aD2@sF&=<^#uaEl+52+ZovX)XZR*`aB1!(fGT`QFa zHZHu8ud3}55|d}r*^)573{DTzykI{WzI%)e}ZaKRt~QKdX@+AIP9$KPM#g!$Ajm9~0GYb8=|&?-$}~8K|RKpyd1-mY2EEz=tJ6Nj%@KY87xTi9G<%PBa}(@O8s_dX>3Bc+ocdT2$cR&CJNJ z)-^)qvkLSKVz*0$T=vAsn`If6RZ=URFQ1;Z>Y!U05z0=V-Hyk$<&^aF#5bZ8=6GCT zGOT%vS8B^fWgalgXmYg)NRNM+mIE zp+gC&W4^$}=zfBUwdFp8xU?4;`;jeMZCa_W> zToK7^*Naw^$_#Zm_<_vA9tPnpk_K>`4W=gVK+%#Es75KK!ta44w>4ZGdk*}E`{GX3 zkK}Nv$mx>Dg$e~01w{l?FWH166&^Yqy^x9m_d?VO5HE!EKq@-0@ckh9K*Ww8rNjda zjMipAfAMf?O!fvg=B`dZk<@NWw16&ceU@l=X(s zFAY1g;Hxy!KUdEShC+A$AAQOLs~HliYjcP}6P)QHW*}#Lc5eIS_)1q8(iIe8z8>Mj z^q;12#J8$?b|`j&T+Z}`dRNH8X7;lR&dh4D1YVD07kX+jh~iz{7|%8wh32X=;)~*c zW+z~MSi^Wu*I~N70oL24kh$EZ;Mn=Nm?ZA?b)mE0|CzEfAl86=eSGm7<-?e|D?mJ9 zf@&?!dliiYNe0-X3{Qi@=eXxK?Rz=ZX0s1Y`Z*IJt!&;D9nVLXbsBqm{5RvvmiC8F zyluMEf9K6oRBTt@8ZRBw!7YN6G?)A(d2;osBnwQX{aNbT39%El1|pi)L{};?#lI;! zH4*YBJ94v9o+il1nQf8XMgC+z&)c$@KYiUIOCfo8?hQNpJKS7;zsbWVHvGr)i%|c} z8l>XJ#+Q)Q217clEJD7~*WN}qSLWXB@{X5+!e5M^@ym}TZpDwNr)V!)flWMFQ2|qx zg~e@$<+l#|+P+lb$lH0qdAgI0LzaK#eb|Sp^s9dnd-p%!jO_3{CN^^8= zK(5q>Xa62g->meJ65VU?e_DV88+%Q3Zdo`LXMAhx`n@s<1qcyV z=Amy4QyhhF9_FfL+r#JJJDFk`@m^!d=m0y0Cezx zEaB-_$I_PN?_vsEgk<%!n|!~1jn8FH{Bq-dKnAT;xU7;@x^IEtfukWBw4eWaCFN)( zQ$tJ*co*=kv2=GM{4H}o$pI#O`0$2edrPFD+Yz6HEj~BazI;yC>aUy;y1n6Xn@vsW zNI<4vJenNOQlCJSxw#SpkO!F96O>UIpI`rHWFED)14|tB!*W)^lq%|sZ@j!8;P&Fx zdR6Q?KO{p*eYQv+UU9;DnBpOL)LPm!lz840>BK6s#hi)cR^!p+j3j*B?OTQDCJsWw zJaipJj<+b4A3lc85iB2K zm=+fHM@RPn(GZa4KdhenebTvxl9PxvdvH)s*FU_G4w&u@LGCZz7^O(zGAS7^Dg^Oq zJGyk7g;;$REE}euTpc$48_B`FGOWOsQDY`ktUrtlWR0jNC`L!kl#h=>jG?cj%Innn znwo&UpZHDkwlnTWQJ@;9nxKTNvoXaVx(j|xvyRRJ`Zxa_Vom&9a z6p9H#(swa?;n}Fq=$&N+_1vy`!1sVl`ox=O%1OA3F_18gHvgk__dtU{TZJJrmiOR( z?kr_C{{(qyX%3|eE2g#;>zyT|wT0yi8ea24-b~^3R}+*+y39`_UJ88%s8V~{Z43Fa zt1EjrI5|1~)Sn;Qd;IN|M33!B`ZO%}?p;wK+w+E8M6*0mmpkHaXSG&WylC|gF^v8n zX(wS1v>O)D^Kn&{0@Fnrzcyt*v+8MopHRFqg2!1pV0}rxS5DI}lhgY1+yueM_oX%k=!we>yObC64>R^M|AWIEq zM$HB;i1#Lf4QT_=_q$bbbVNi`0w2VqE}bL`G)?B6Ix{WJ7JgENi$E!U1(ou_``scF<%^YW%2KYAfYMfnzvO8dcTVGL*2jz2}3!qqgSf#WnObabS8 zM4p(~z`wRG%PK1sLHj|KMiQrim@k-20J zuI3ZJqs!V`T?Ou_kOuSm+`)UEKkoO6m9qnMC3SAbD*CorejIz3;lM1#eCuBdV%FDQ z1H`yZSzSGc8}PIIL-{wA&C0F8UyRwfE>>C!U{1pJi)ml$VGlohxR>N7r`DdyT9dD{ zu`?bqp`q(fUNIlmOxGx1>$Q99y~*d&V=smXjTx_{7xa|BKe4RAKuVQd0`bP!lMQ5J~2dE z3|6G$XsRI2DSqg75viQWKQ4~p!3Q&W#kf&)&nSA$9y*qo)8c2C%@X?sHp82IHT7+s|8CGk$=~RW*^DMDC`$Rs&<-K%y0Lql{a!W5ZWgUt!Ny1M7_@;~^%Fc@0@ zdbINX{_{tsT zwR6)qLQYI`=U)TBGvoKV1bk3<)Bm9|=y6DW)qj5*>Ok1D4CaR%b&>yer}6Hl9w4YR zYT?jNneV9Wygq}!z-YY{vUowgpMCDW?X;_f;oGzmRYOTv5LO@q7YbJ_2xn2o50V7?}}2h zU4X|mPY^ss&z*bNZ8J(V+Th84KgXP1w#aW+?)m^8+#94iwrFH`KnoP=Q65-lJcK^! za${1!>ysvPt%^$Ut=z`%yIp!X!d-D7E-VC}k}khQ*`AzSg(Jy%%Cf7){MJ97D8u(Z zUsTUl9Mg1kgBW-6w=8JlF$6se6J>HMT3#So3+%A!wwEbAzKb-d*vsw(zvxl^xv$Iy zND6LUfzEF~S&`1!$R)Ha7!wLl3%_DUU^8t%!}Fw?n-?Q=C~x)Y6h6ig1~D3QXi{0( zG?UF65AJ-PVl1kHdasks*M=UU{{b#}#@+99gMfSC2fq-1g;o&>Gg3o~4=+MP#9$|n zOTDY2!nm>g0C^a?;Y2btPxo(b2#w816y5saCH(%=F!A(kdc~?0pdldm2t?nx)F)Pr zL)C!!u+NXM8*j zIYrQ*oS>?Qe5%SsHQI`U{Q+j$>Q3#8r$$ml?Cp3*V2m;5`}LHStw~wOmMC||C-m_S z4iw*ovihyBi52K;CLQTaD%jBE(x7J$vV9rBO3I9D9-|rUH*i_7`!!81nMBYI`+3U1 z9C4!DS`mCv&*U$b_J02RKDQr4DbyI<-IY^?OVBDJw*+2RHUgJ257(<(L2EO|x?1v2 z$MXIj3`j)E39{=t%k=7>2G%%Oa?cdsx$}vCva7Wqbzc4;Ijii)8716@jxO0oUsb5x zjdOVJ-{aq(-Rfs~*3Pr+iqr0je8*ukQBhG+Fb=FTk_NqG70Us~ein2uI-qwRE(~NQ zjE&FD{z_cypH*eQbTB17FY{T)&nGqf!m$xHM!u1eL-;8$H|RxpV@b?1T4LGtV4S}l z^7H2AWtIO$Pw0{1cWS0Tyo_d^PQJUFnwOHPb95CD6AWP&7CCTbbODr+R_2eWgwz;j z-svh}Xa{8TqI2vW>7zhGBtW-RbyJhqk`&q5nruKOur}~&u}7(Y_oMb>n5rIj&^VQG z@L?gx{`L#pRQ1PY=R%w$cKVzI?hUlhu>>gJP=RzG)ow|d&d;8A&`pVF4NDKKmTEj) zU!u{Q#J*`GjyVSXQejQpL-F}^syY$Hj<-V5N+X}6jj)~|>0{#mvcsP@D!x#hNn1J@ zc}>4;mFvR~Hj<4v7D1v7o=~$O7&k7?4T--aINiQEn*`Ln{aR^*R98g$#YY+)9jf-* zou67zR<_^g5<1-B`Fhn60HzP)%EX?Xn&}Sv81^y4paQ9#VK(NbyrSpF!%q%6@BQ3# znom>|H13Y8`Mn)xXYY)B#%SXQ(Wj?p5P(yNvhlEAB1vDf??ZRhw6V^FtK=k&oMo_{ z!aW-D+rm8ETwG6CYZ}>!a4K;msY)^pG!DyYXR=Q^v$^m=AB`rNVL6$whoC_dKFCT- zGp(-&;gPCsRz5p5<#g_xFfP@1`L2+l)9h(DLyEQ}Jd>|NxI3y)!Jps$aj~vI%FRhM zv{s4(XlwpJKKtWxf|e1}PIAa4A-b`SHZKZitVB`?8Vi6S09CIE2*3t`*~ZbmOc31- z$}BrsWeq7GK3kmBO_0TQh+*EQ_zS7c0O>9^!^^+zmv2Xoe^Fqc$H#^w>QDTb?Iops zU5HDL#XNz*yvS9A_+LF_Le~~alQ&#Niutp$io9$D!5-T!Wzo0x@!r4akA|%)K0YX6 z!^N{jIk*DPL!rUj9JOZ?_rI1HtZn@Sr1m6^P};v6Wy~?OM3e#ftXMAF(yn@z4cY9r z##{_64K22m9XQpXKF6xdO65rH9V%faz!n$IgsaHO0K>-Q z>8N{TR9(uNP`WZzv?Ep#_CgZ(1j(C%bbLQ=7%PB$L;EF8g%_EbaW6U2N&f455utb1Gp?z<5k%t}L=Rb6(YzgV&h&)Kqq+QZNG|os@2FF_NufH1lyr zi@y*D(5Rj}V=w2HlfTQCriOFs_CKxX{30M({W+a^3KqURb8`$$5M+qPeGQz&4H23w|D0aVR9Y)@v*CM?> zC0y~D?3bv<8Hi^B`#TDGNwrd&zYlbLG9%b84%NBfsVNlYvdW9Xrx1)9$ET)}_nxsH zsgS5&nD7d#|0~z>*uJqqnfxf5_qQ1((!QbVe^zwd-0OlA=j$h*2>!1}j=yQ$jtSj; z@tyrr?>1mw&0hy=C-Soc-LpX!y)$K`Z&DB&^<2pI21#Nq2ci|MJdZJ25ov|@S z)ZdI z2t{7kMF7(WWkBPTZxxDu9CGYfS3zs0YUBffk$BiB9s75MOA0t4Wn&J`A|mv3Ot7Rw9X z218y3_tmf{mN*~|0Mwh5EM5Qc z8v8uA?d2L;4U%UZbIp@(8x{iB+qVZtz1Ooxmxm-as994VWo4nUY&9i);&pksZ$t!T z&$O-Li@QS@j_Dv?2_bC99LID*#1c!AV!y0td>+THwPCZeB9}`9rvPsN2c1EST z8DIMh3mx=FfE8i-QF$~8-3_K}EW+vmD1%ILEmc*iCa${egjr#AmApkq(TWvkyCoO{NqD;~un7mv&8#W46>qBMso z50=9dvUC2=OUeSvG9Ac3j~wFtOuCl|{;D@~0eBVm_d$ufGf_oJEqt$RnAVFsyD|BL z#D?G+qO{8i`!3>k!i8dMRm>_o^0R*TgAX(~rQK(kG;4%p!;0_P$3ZiNXTK%oDzo*H z2TJ0eyi)yWl5-feCCwgMcz&FmobhPj<ZQ+$1?Vkb24)R`!j2{|HuBZcTL{oF~zCr0@ty9@u8N$*-_DLs-bTxu&79P&w z0|bzDjW{xV467Lu1R&V$>vQ{`Y<7rA@_#o58nQ!cB-zY=#g-gdm0-g9+LD3@ZA`Pr zBe}7A?cNcNAJFARxQp?=&Z23Gx7br;6%`)sB3DJeD1uZzKKX88tQLN)%sxGznjuJT zw`^u?=S04mmU-3KIpcgTn;g;D6gtIkq)CYsa*2Xjb~V5o#(77eyLu|~6A)&3;*c$z zA!n&cs)Q*U{3>i8NR~~P6BlF&SJ}GZd10-DdGf2OhsLd1z2Xz(q9+M7C-`VoA6|@| ze$3K5_AZ4t6U-z!kI|Pf6msl!X=xnLJaqD?&>Uv8R;@d~;3iHa-hy!!;D0b&k&!T-t?uO>+EK+iXnBa z_ggwE&o=#~l@94^OjSFJvk>+JWFgq1I9Yp1c;&-^Eaaj?urxn#pudK|eGSuXFyokk zh6Rgk#;(a*RJ&{1*!+<@js7n-wbOLOxwxf$dEVY=n)FXpJF?!E>Kb$9iULi5T-6++ zPk_iODXm9DSqP!+44^M;SXSvVsX!*V8a%9RtI>blJ~Sjm%^)c%huVxj>$895uJBf`tbZFJRQ8vO-Mte*!;Z8f-Ban z_oqS?BTPvvkuepTkqs=(uIF<>$;Cq?`uofS22f4?>mSpVlfa@z`74in>q*=%aMp** zR)me2>r&QfMcyT|e3MdjeNTSjfnsLtX=k zf0mB@9PssyzT!Lm^T!9VVuxXR5<>aA`O1MT(VY}K#0|3zXo>SHRCq6;WWO)5nkX?W z*6$h(?Lu;q;~0|ArV?`O*8{IA=L(OGyuzdL*f+-I@AioQr~u9X0ivfKHk#eU6adgY zotR#tW9t+sRVMma(`e?kF5yWXfv#OQ<*zfOb%3j$nz~jwkU=HeW@%!}+W>D$cE_7N zyT{&zT`OYRa`_fVduE_^wB}rbH9kWiNY16Z^K`QPw?WbDVR*zv{ozO0ej_Y3G#LcA zX_I)cs`N*=vl1F0j!DUO`LXalRXq*+XknYRE=weLXd_)}dR{iSsDi>HP~;d2ZQX`! z1-WnPUa4xld_8)27?wudzDVALaNr3Kw@p%rxHDNK(Z4_bx-D-Bv#3Ber5rBU#6+Tc zIu!&vzeVFGu(+k!&s+RDIbFJ>c1E+NxJ1%W?{i?f z?>2*Zp5(^HOn>!oE_Q-LUDmfuc{)FTReb;}M5_9BauRf$Vln_B3y>br5MLCJZQB+E z6%R1;KYza2l_()4y|nabOe5;z9-{9KJV9LCVD~i&t^$Fs|^ImH^aZTzaSC{ zPLT`ZyuG>zZFRK8xpe>2F1>9PiSxgpOhX9@U|Nuo_tn!#;NRV%M|dybio-WmQqq3t zktOdSteD4lRSz8jRYj(tkVVm*-QR%e4Fwwb`g`1G-j44-9hv@qwiX?qV4bUDT^7@M z#ZCLQ+Rv$QcLnc;=p;%$)KcT)z*}XbnV0%mFp%TbG_BGc)J-qww&|T+kPS1zLw^#V zrT$4>Dp>Gyt+a^2edcFL5}c>Tb#X5%Hfse?#3U-}rWswAooCnIr;(j%`gp`a5)~*F z^I{dU>@4%mS2iE4ug#cjZixN9OOH_64Te*vf*c()PNl=68W^2j%)D0LURyH+0l+E-dq7C=swrVvI zDf1KkNLIE*Q_g3B5R+wBWCbe3ofoOcC}qV@1U6pzhWXB^{?n0axqa^q*BZ31GnN^? zPWVudTq!?4dtE(oM@LSIrug;A(XpvAJL$`QtvVr`y22ivbvA_*DItdfXv6AX=>7pe z3xhIx&ay5fv!4Uh2#W&J2*qa2E{R2QLlA0$F_SqI+shc?yfhO4*KT8Otello1#pz3 zGb#N1%FhE(Li)te)%66{CGL{Jl?ia@n|x#4F^S60^4h1uaS*qH%eZwdi*rwpv)%!T zdd3+_v)f&nGZ(4(-r5T9@&N(HH?uc8`=hC2q@Lch-ri2z566P z_xZMt5Bcu5vHRm<%lq-s-g#L40f43qT4O@|mUq7(wJ9@}vKIE4aK(RbJw+g{@Up z>mHNF6;};n!w8(Ra&KO8avixY_F;gJbgxrW%Wb@9v(!H#ke~OC#@viO(C~EDxl;Ng zjMm?qw>alD)9gvQWtXit&lphisTUIps6{+59Y*m#7Q5FpZJMk#DWy87$xK(IcFPxM zoO`d!<6j9i<=(ya@Nm@yQ_S`)O~293;1g#ugEsNAJXwXryF~d?dQVT6xth~nyJMyb z)pP8`?-yx`A_WcXf-UYW?E5&Ks)uqAtN3Xemny z3%v-PTGUwu-=*@;E69dvxMV|dG}`UPC;@xi_%|9dEVd~fxd44q)ztp;A{F+wXY%Uh zbD>xYK1qD-BcnswJUejhT!?2%R;%YXm=R6u;AZX|CI;wNOpksMThm(Kc&uGs_BD(i z@)PVfB&PIw^K9idM4Z3W>ZScZEkFiIYe>i4?R5<0@MZ*yM~LQOSJPf`3B`ZmJ1mvl z-BXsKLBiJj1wH~Sn=gF;Mrcodcg+UG8i=abN=mlW>;^Gt!FU#!?=(R;P&#l&} z8E0;VMi?5d^W|re?UI`%;#|6o?~wF-8!||T(VUPv_|Qp`0?U~LBia&G2UWSzH$jt> zm>BHmcM#|wHbv-x_Z<*Ad%ov0DD%yh=C@YT-N}y|M+PRlZsGkNH^%%GNlX)6t*!m* zvDR%4o4AtK+?lpa4F>lEk5nhhT_{v&EUeNK(dCY(#*f+cF;FNf9h8P2h$I~@kwa567N+$`&`(%O62<3gRB>$M&5$m;1aH8xJu-Vpvc zI2hDZKW*F3b)yJ4%6K|8o@`u11pn%!YuAc;1qFm0xbdkZCVDxQ*5{IRoCCf~nK6l4 zcUo#YLL2@lfV*c{WzA;#oJ&e4wwP9L`bxYYr5Ai1 z90BNNqN5p8Z@2NW^5z*>cI-t@IXZr`nIOIf=Zu}$#7*>L!WDzGA6!H^dBh-3r>wZZ zTr%HegcIV2qwd}1fij>KbFRbhxd}8|-s%#d^x~dS)|dXt{j`y;!{dmG4`?jN*(+J!clKa_&4nHN{IwS=x3PFEl^%ks9+QH+R%9fCmkfFr<<9o; z3dZ=Mml-4wya9&~YfrqOB1iEL2%(>R|u(#@^~V82=MxJ59QH!QZ&jCQv< z9JIsfYVk6W`c z0t@f4mp&#MOFDU=Lc+la_T~E96tfwdpMx+{tJs1Kx0z zIeBAx1W`sB%#o!ke-h3eV!$~y;m<)qp@`T;av%RIQzI=E0f*||cJ+CMh8%x_2fn5G z2Z?T{icZt;JMA%GN(SOzUm#7#$WEGOdz(N}sPVKJjZj)hYAPRu6aT4TYU{SatOv;w zY_~04#a=gp;7|u|d=NxTY`~#W*QtDaki<*<74B;HKTqUCz{^bEm@?HEDx)iYz)_Tq zDj~lOS!IFXQ?L)h`-dizwym)M-|Mb7t8hLOad<7==zHGDG8!OB4_}Bvgek^hY#!rx zv)a;xl}_tF=ZNHHCQ`k1W-1sW%g|)gP?k@o-I)c}u)o|HuA~^B zDpxGlPs+}|)c6VhF8o@VD%CKGbsNzv(spPI-fZYlT5rK?M-o&VmT&3&)zhA%Z1prQ zJ_MuK&82D0iE~zu$?R;WwN-X+UY^?IcY(KGO1Yj9P4_*{EL5>sCp?Ix!PVH; z_t;z0*7N@-E|9@NKx!&|N27H%vv0pwMZD2kzBfXo-p8YwWH+zAFtKU9$9HAYe3L5D z%GkVT(-s|u&>ta)65{$M^U>a!qdyyk%wY^gq?D6wY35E?zy>BJCYXR7VHZq$g5a^` z2CZ_;S&W{Z;vUOY$7-jXjn9p{E)R&48bv`8fByXGTZfp}Ugk0UnDrmN`_m&*RSHxVRnp;wxqV$nbZ7xD>6X6C`p6$^3t3 zw=o0kHD+^R&mL#evVEn2(Bb0UtpRBZ-|C>2s{k46H!#i|23`P4)}N& zRZ06HY42>v#A?f`oR%1y*k#$I*D*4-F#fm^3%eLy-Q z=N`E9Xit28KeF1=GGOY!U)`gxE`Fi#CKO34D~CI>*HB9)+4wjksIp%^i8-|Xy%VDl zNtmO~l`Xo6&-!yyUHkfbIZ4V*OGQ23D#!35b3@7{n$RXoflP;iazg)FS^pqBrYIx*h5|-J$G% z+DnN&|C?&|LfT0BAcYR0=1u$8l z$GdVIT49ntNbW?=2maN6t8~a>W?=Fol4}D>?*6y;^VrGSYg3xy#EIT3r@cR*kwx0| z@9cpy7rcT(JSVl5%lg-BoM-xi8qEfA(awES8wJ3C5_$xT+0_|miPeA%{yvTXs1K9Y zKW(Na4(RKy=! zu6T(ZSzlb4COC+3VZ!#jn>>s(4zD^%;3C?wgC1Io zEGmipjLNmT@vb9Vi9|nw!&7|E*|ZS8*BC^}(H=gc5^ZC}Jn05|8zePtJNm3TA`r>} z@)a9ZV`GDp;z4r!8LTm<;G8Q44zq3Nvk^`3W50goj+^9nD}YpM?$Rk7*S=+O8;Kh@ z(=)lrZ0ojYGpuELMl_nKN)|=X9iR&!$?Zr}Z)3#aBezE2gTzsX!Lw+6@dD(WP<&$0 zYHW-nOdU_-t%$jHzCAJP3$!UG5DFb)Ug*>(D8!xBiiX0@=f|*D;mCC;ZfHvhv*U3svh<%<3@f6>=Ge%oij_(X52Dd_EAxJ=_ z!c1^^PV`;uh`yKC^-5IHx}v^^QEjZNyWBzeN(a|mvtgW}4uAT-; z5jYvv2oUha`UY)tZp*)8aU^@c$+Q_2x%cRDbd~Sga^dD;r(Egnr^r8QORN5AqTR^% zB6`nk(&<=brU!uj2rU*nwX$tvx7|IPPxTCmiaf+|d-Xg0PEIQ@xTkT0Qw52^7iIQq zN<;s?Vlwj}enqq45{>i}BG&z-k9HM~h_dh6YM8A`#J!ahdAsVEdb7dKJ2+iU>%^Nw)q-RT-p(uhfoBwN+ z(WA-k2=29o{Sw(ew~Gr7nR%a`+u=6`Werxwgyz@p+Wgb2FAXQ#HV%o#LT|gAGkgqD zwwal>0#*viYPJ^0zXbevRv_t_&-)33bmApT<4S0Uzq}}x@!sUcTZVY?TQ*ZBH*VZt zTy(y8@(rw}zQ;Ch`|mwO2V7?#g}|nEdnOJJ6oyM4e`*4v-eqYut9Icjt4?nugFwLd zDLK6vDTT^coXoqZ2y&*4f@u(cOZ@(D!0TRWDl&AO)I#~e;-D58CV5ozwKOzRd;tzU zJBxzl)?Nc8&oc11cV}y?prmcvdKjk>glwRk(BcYddpboIc4tHH^_|ZfB|!uC>n^Og zmBp>E^*6?eyO1u-LmR&jcn65PFQ%<{wWTq?q}^kRTTQnjd}hxZ2m-8$i7td zqnDIxPfJTj!nCr3CZVvRA{5&^R(561e7Vov`y^kgNZq@ZG+ttQ214#Dz9pR%6+U>V zkc~~4+h=+tI$9sQ#@BXD-wvx$pJV46T9u4Y`BJN<6gxy3Z+{PDX%N+{?giJ)V7ryh2pkP=fDbisT7^PC%+5>TgA6TspC?E&Un@f;Okl{Pd`Tf)a&_ zUto?%%79rO^u+nxSXfLEArop4-bYRoY1X)IF39kWry>TLFl7Fa7D*F+xyR<1&|x?k zI06$ljU0ji6X@PsOBU;^#MoOg8y$ZN!>I#M0Q4{RYK{dV^#buN0T7Ic42QA`BopcIg%5{vPo~j+Rcf;J4NkN9RiQ-o@Tki-^7#ZY$NKN% zPz*#WjUX*+-);yUA>YC3_-j(XpYZNz&hxj8`3k_(RAUQ#yn2;XQ|)f zxecgV2OSx9TKnI>2W%GvaK{^z>LMajn{ul$3X+Ito6eJJQ2i((C6imB(%yWGt^kRF zy++H-%GBdD37t~~sl_IcwquIMUlRjCT4(YuzVsR@sAD>;5O#>BO$fxHtdNpk-}Qi* zpMpYQo}pqk{cl6|-<^+r`wIv7%LyJA9@VH`zkBy?+YmH^0W~Qj!W%tJy?#82qU7MW z!R^eCj*57D9aXMQ<%mCD^GRke#BsumBNP%^G}|G1Dt__R>%EHJyIccF%s|*rI1qje zklIc3DxRr>GLR!d*zhttJM@6Z544F1BMd(P?ZC(n9ax^F)DSKe+4;7-ubi3ejnMgi zFn1W-we;^%x%72~9N&c_skLp`*i94!S|F5V-#m3;zP>XWTPDn?S6XmYWo7G)M8?YK zZMVxPud^BR`SB26tNIV!p6M&lXgWnl`X9oDMJXnPJrDT3kOTqQBuH8a#7MZQ>+5{^ zrl9W*2>E;Tagki0%&;)+>d?qdiho5)Td4eaX56%dcq2`Fd8BnB5EmFB)TbO>R$A)G zC~+Y-vqv|vThD=e~#KhJ|-qZAXbPit)1?*5j#q;{w?`JJu-xMPnXV|ct#6n*B3stu=8HD8luSL$}qeEH9-Ch&oVcMdttNTiwtEHu7wm>&xV%!=i z7It?_;+W>QhdXMV@`@R1Y=zJ_f&juamfvQ<$0|a`#*QFgUjm1mSMSGBw$tXn%mOn+#s~Il$0b!^V#;pM;DA&=tti^oP%*pCAvj z6$85w8(BMF3L{bgTxCSJOm>HwR>zm+WzNEWukMB?rE{flkMj^Q_G6DX z{~~{9WZ(NX;R#1wXfHvCwne$mH+rLu$Lj)8I{#6T@0ZWRY^kdojUMk~RikQ*IZ^WW?~IzA zpUXkm;21YAJk*|3+Hzw<* z$jFCF5J>>#-7{hv#nM_=Uf%M$EKV1a1Y@pH^L0)RBFT_YuOExAUt-Z(`@hj?;r~V_ z3=%29k5|`sfB)`0av8kYk9v-ZFF>9clNHMlGqJXe9Ue6#PthUZY!d}B5$UGEF$Fh6 zTwK1wyH}jY{YwZj$(h@lOm(2zN!9G2-m4Yxgy7FiZ#&FSL#{r0y_o0BS?XHo=FW|S zRQL!b@q5NmKvodskc5f+JFLyIO?xgejn2n!W&@w9c-UoqB9b^)+_WzA*f#{%D7qm>rhMm88ywqw^a~I_xGr`P-*2w(>?le>g7$W#+1n`_^#|6QnAzJuPL#GA>R&Gn znd&@&fFqJO$C~FNsFR3nr6igJJOy?qE&e5n?u+`o3f!w=Md;W-PF-K!q4M8shHkeI zn_V{)B2oLnr|$XlC)xC4rCqYm6%XB=E+&q54uLREml8)Lr?8VL zD)|c8R0zgU$SVZd&y-12jEQ!1R%{+c$-%AZ_w$^fia`65!gMN9zgfCGZ8Uq8n7BW_ z>5@ULjhYam&u&1YaXA4^CJ@a7Yq?!Y;E#aPP9o4Sn`8nJjE!{}a>#6)f5z%pQv)X* z_1}=qSyPSQB~g#IClP3WY^-VRHOjYNf=Z5b8L-xaykvufP&!+yR{L5KmzOscR%s+O zo$#0Qu|Fm8Hl0|Ss|4*+jg3u5Ov{85Nlx0`jaLPBX43cq`WUM+o+=n{N$(hr!#IyQ zU(zR!tU{Uq`;A4r3MuyR+My%0!M=dZvg%#th${&w$>?KKN<^C1I=!DEK>}V z;bKansm|tghvg4aH0)-8w;{Etx`Ega658$3(^5FRlCQJ>huQv0-4$v6sH&kH5*3WI z;a}{<8-2u{$nAdvjbu$V_<@hnOM84f3YZy!+}er@3r=-pjLc@%b!y)u6)qRLeqlzw z?VE&4C)FB8iQ~eeoF9ktnhc>tMGQy}Fh2lT$@DOMHIb)J zqofFGn77fsI*H1hP^Lj|!77Wa8)c(xTJQgEqoM!1jf63TLvVnEQ>MN5$V6j<(J4dO zw^S~zDL#GsIY`r^@CIQ-SKlvky`J8r`hcpiN$na#WXP6S$ z!bKJqNWuUPOxmI)xRoq#)L^3xTwP^HNzVq&Jxh&|ZO4{xElze3;YI~siARhXjA)*f z7GmrlC9y#gNU|c?zXvy2O|8VJY?l>}_XPjib}))cnN-whWIne1*?i+!|4M9sUyE~k z5R8&Kh)tL<``Rn7aJyTPu5FyLGAO?P+qdfE3kz%MM~TD!u-mwzknq9r<3s@0$;DkH zG#dT{WVF}bJoB4$@f062)iL|0%0$}fSy-fQSecn8AAmC%p>+=Y?`+M!2Q8U)Z%uC; zx$fW)3N|=-WHUkx2tohUcm7A0k6co25Q2UmQCxS= zrw7|?{B81n;!z%~t1{u&vFT(L6k%=D0JzE#r3#;38h zqIGzM(Z?qfnW*@K#N5xspbn7=P1Z1{rL*xP3K0XW=2WTto42a^VVZld!@)4robW|q zX$E#nA%3#$b1Pw@!3@Y&^6FJeRoAHx;h6_bIBs8vf~AaiuH7ieLdr#l6@*IYrg_k7 z+vj-Rog0Bl`X$fIA?`AbFz3~RDHyIU@~yYK^t=vkYA)i?*o5DEF{FpzmGLIIOb~18|GOn$+ zh*9vkBn8cX{XZ?hA#F0&Q^(fS9x>Ckwzl@RiW4`!EG@0~wbySGioVx6Cg$RT0#itV zMt-%}r)g|?beTpcgt3@C|F59n_vPQdz7fTH(=`y^bud(hK2hbGQM|ci)W^n@HaR!9 z9#Rp+uN1pS|BtHoj>r1n|HrjOX&9B25HicA(lVkbdynkQY?(=lgf~KVl7tYt%+N3r zviAzvE7>D{_p9^${PF8{Zk^7#HLmM59?!?){<}pQZRuTuVO3OuBPv z$rN1@4xfBscKm#hF6Q+Si;})?IYz@oc0vTFRbO}hW%+b(IFBzvYjzQn7QGn%O>8eqO72K?g)@=fyy-u@2&UB=wtWCY z4bDojhH2(G%1RS|jP_lnAu%yFHr_fUlcv(#t;IgFbnw8tnWx3arkz<<`Z}%&#*9w3 zNj?0`3a?%$MvM$Tm1Kpst{ly1d%#2rzY*YY^74kWF)eq$fMq#UfjV$BPZPONNM@*i zd{F)d1*_e(D8S^l^mE4rab-~D{T_~wem5monpG8yNkXiWa`nE~GsiNYyK_bw01ICDs;ag=H;KgYE$ylK0*4Z|pFN5_4Iq5+d7XRQ zmd<|ounfz-eWmmlI?LO7!`%%LAx zX%0}>+%f(|ziU^IZOISf2(;ogS0HxJfKDzYjL0cUQwgmSwPlpa?+LbSB}*HP=B~)f^m40jRDs;l7`)aK!!9Z6S2o%4YkKxRbG%EliV>^w&C{Mt>kQM zDX^Sp(!0*P#K8UK4)_&dQT9_m9LCdn?Qn&4PfqgR{0;J<;|@G6e6;o4wb}24n%j4` zt8g|zR)mC<6t-^cI8$ycz~ZsoQRhI|i;kAz&!#1wp;h&*ZP8sN1+g2V0=Pl{VM?V$}KE7fB$d10Ej2WDj_H-9n_7a&hN7wT!d0gx5+UWrA=G6B(dC z9bKoWZl>kL>Vec(*09eOZBV}jkBEqze;wCeT-@eobb|OJ+J#(`y@*T zNJByAp>8V71}#iJCd@E(K-n{zW@sAe z!S;hQwXOv%wI4LzH&H7TBCZ1{ez6_~nU%xB}~tmv^7L^tw~`3+^! zKVS;}?!$4l{)aTicGO~FTN{|79u72ln?YGU1W^Fc8SFVl>7|Q{Y1=O<=+Oz(nFeCl zKx#qYh-sCsh>+U@Vt!JRCv;`rvfkVIJ)QLKlA&iFP-b(r6UM~QKA_OdPmUUuEo_Pp_jPu-sZDZ((Rwl+IS`@53t#48W`n_ zcNG(;^E9ima!5+8u1~u5SLy3Laa|_H24b%c;C$Ds-)fY*OG@b4N3QN^T59X)06!|b zu_=enZ|H~N-r{1$pIv6#(G8xnmjpz|BeWjbz8R&WjR$NUgI?CTuGg`ONblvS7o5M1s34))VWla|vw3`4~fHqOOuUipwJ(8~Qje@2lOsLDH287j=PLd_smmyaG%k zb({sv2((CZf2|~zwxp7&mg8Wy5AF;A19xCNd#;iW!wi9kOut(a_1-9@ofW(9SwG`8 zmD#ZdXI;Mrb{eMRL0TWQmWqQljgk*$z=4MthL!oPBb}hwU*rB#<>?o5^a`P7`HZg= z6-gG?hLpDSbmOB@nE*@yd_@yL@gxANh$0a%%?(Fij2 z2|-0}wbyk7T9`hBcyHQaDEEeFJ5f{BfSc;ig@w8f;x=uiW-J34^7WlB^pARx`Tc43 zU;REt8`OQ8{Ut{yo@=7U7dG}NTMP_KVY~j!Y`pET1a80Tu^$3h*Z^PA8ryeC^qYG5 zIxSQ11z0ZQe&YGSHTzZ*PFcJA6KH|B5TKZMmK1E{&$ZMG3i|Ifz+&beNC&kYj~Wp& zi;E1Dg`n~??7xan0Rc?d7C{)s^xrSE3%5C_kij}WVb{_#^`^9RD}Q+N(&nJU2g2X) z3^Ruwd^P|$(bQ^u3U+A!jFD~_>WlMil`3f_7N#nlX_{!g5mfl<=cr@M__U*JVjj$7 zu2YnCJ78H+G2CSmTo2;)gXi+a*4i7n9pc`4+;sfM1+98JEASGka1O0be0tzgKZQIa zv@=LUILALU+*@?_Ov0cIoYtG?z)dc_$;2DQ1@;(@F*I|>^a?plj#C_P#hW?<@w;h1 z*FGkHigA1)&Is&wVSRXjY!C6A{h}CMON}O+*W|978dWL2CCpV&7wp7?%Bjlt5t|F+ z{*(XrFh5#eMxfIH1cI+fl02g0ef4-RIgSDN%E6KvcM9ud!+VW4u6@l6Jlan_Ia!Ad zK9ufBx8=U?Q0Ab*R{~wpz_Im-Qrgenq7(Vnln#B1ZU?n8pBnS?5%Sy7u_5v(R?Nj_ zZ@6r2$_)piH*Z09?8e;OaFB@fIogXKm)bL?2fSUomg{~foV!%+KcHVZ3^F=tvw6zr zmw;cB+S1!E*a#i_InmMrL3Gr9Fb1)9@>9!*l&oDsQW|Sg#e~lG_Elhdn_i032rGi$ z82%&n8IFXw_625dfbzG)|T%xJX6 z=`ke)!s8;l3`@ysTu5iLpJ=}D zkO@zCO3B9ZOc9yiLp*FaPLJ{ZAg07b-yBS!A4FwGT2dOJc=Tb0u0xY~jJjSC+y0S= zr${z^qm|k3bfvYvmt)6$TO7c%EkzDPqFZQMxSmGh#ARXe!p**TaaJ|k8gmi~8d%~N z5=w^~?x4*;qfsLDzizsp9j^gon!#W{Y3KJA`A+E(3xII1U(twH{+w5 zqqbW+u>UoxuIkE3_8m3dhlkOzm?^+VI+@1zN=f>0_^cSXcFKsxAu{QP&qC7eFbh=; zC&5+cUXGobU5|@sP@5CC7LVe>%B6hAgOF!uM2T8qmLS&f%a}B^Mbf1p_9mMgfzv?H zLY<0>^dZN?_9_B=KT?#Up%ONvQch8Vt2<6iEaIdQblh=a;Lu5litt;UNa0RgXjL9~ zt%2qwX6+ZdcLWfaBX`BnO0rAj1R>N?%-QPd(LtOXBlafsO z9ghGS4E%i8Mz1tYGeZ^m6=qoY;$zzV`bP8|wyveUWx3M;hA??8Ql6!nv_;gl@-%lfc1)LqaEl-k`zfio0oBZG4pBL6Oc~`A{SZHa984(f#6~iyuM_ zbLv`Eu3H@cUwR?t45%x2^Cov#!Z`Fl`iCR-WI>~|Qk3yOt`cJ4jFf+~Q6SbS?3)>X z=Ipe``4SfwNYr#tn%F{Vo0s?g&n2ez_)Z{#MR3^}6 zcKScY*!ZA(Tbl=P2WD+Vz~81K>A3fA>Fcu>uGW^xEc-PE)~aZ^54YnC(cR;n_} zj{)!%OJ7FucgOCky_& zFx^C}law@K-Hq2umksF|wcCT#z#+h8_m0FJc4X z^5WJ|a3$$>W9u&si_mApw-DMmbndzqgH~EX;ZvpE!Ua9jDM!Z|wm13EZC?2EZCI4v z>CsiMt-+I9$7lk>Lp*+UVBkkz%i+H4h@_qX=}NWEwm*vq$&k>0h?XZPNSIeQW()xF znd5p%t`F_SWC)Y&iMJM>c(0Z?IyypM3iZ&_!8<3}Zz&9|bU+n;2wzY~q3%_>NoZTe zEy89z|vB~)vfWjiR8hB;kqGwDG!<{3)_JJDyD2f<~-IUsvevq)DQ2U%-J;H*SO+b1~x zW}tC{q`R~7u^HxQ2WklOH=uSbP!YA$hdoeAi0!BwW`(m)<9T`XWw3= zmrP1!8)XX@`>0pl?6wEJE^pMHKq(Xzh=3&7SoGi~e+1%*6o*yQD^6Y#d;N#YUs)En ze~CjNs2z!xc03LfMNaDJ&L*Ggyt18%1rT$i=ZeiUOWnU$IZj-tCf|Byd@1I!C3L zP*ELn3mO%F$!4@Kw=*I@N8C5N?nbsZ9PcYVMT<3EGBw#AxL(2baB(?HV$1csg~G(xaH$`B+%YYd;aX1|8PfqJT(7J@}0fY$VesZfCe=3!ux#@e79v189G{N1wgJI_2K^ja<7KE^i?#1fA-1^S|d4iG>4LVKj z&_ZQehCp&lU)OzR8c*~6Pb%%&DcRY6uN?(;3MQ;sbuh)mi*BpE($bsF{tIq_`Zi15 zQG5-`X{4BZH-n#1XIBy|13~!5n{nWPtNOE<_AHT7y}DtY5cgJt#>1qnXQ(PT9uzD9 zr5niU(!o{SKDLj7-f(;Cn#V${eri$@W46mu1Ka;vBklRwzi)^jOz2ula#=qZqAWDE za#t3%!~CtomORF<(P7d!=zGN%5X84oOH86y`0uaqCYFq(UivGx?~s>kTeZY`lRmn5 zJs^#ZEiCw7pgq|w^k-J?h8YKQK#8; zf*{IabhjeMafuxvESCaqM2#Zuo}lGd`Ko#*zp2ywtEs?Cr8D4Jf_426!Dfcy{6kPg zy}Es`W9ak2eFk3jCQx?Xy66-M;R;D-UaIozln`*z z?<46dI`_CNOpXEk1+xujz=U(mJm{ zH<8+aI4fe9F?!Gx`#p#IZVCBh=6mn;jYL-f;0%NW5<5vJA(3IXqR?G0bUOYJnE%tF z=q)uYphIUEkG9}jmhKF)&Kf5^j5}hBb8Br4)LxEuoYOeRW60!$!WC5 zv?vpks&s}bM5nJ0e2k5b5BlwUGOl9o*2@k{L?>qgwT9_yXu>4{N~o}95dgEZIymUa z0X9%RR5GxE74E@WsI#mz9z5cT&%S>V&dmCx-Az{lkvesz3=f%tnFpT6_F-un=Al>D zr5!?IEj-m#m=nOB&?$Q>B-TRp=NCUQJ+}$63C9FzWzsG)9lnlwi_8kdz}|oXGERDr zvelmGIrafcmt@##_JwL%8coIl|hL%oy9|6!uWV!g%Q5;x*Q`)Os|5p)cX!?VJNfBbk}XO@C!Vacr`O&)FE-hcDmowzov!@WEhc& zS!YZm0hPINVgi-ucHuRvNz5RO4gi#@nD!h7E5*e+i%Ugte|u5CZjwk1+y|4|H)Aq1 z8-$x2_9ap~8c}dMU9a8qn#v@myFFN4d+%a<46uMm|8W=x8=f^s?#GLp;_zD0wXZL8 z03f(`??-HUAoYXI$=5}w)u!Be@JI1?%I0W%Hfa3QX{4Q!GhU!<`8MGNX&FnG=bSTz z0h{tqz~8_mvv?Iheq38otS{Bfd`dCay;iQLrB$IgSX4Bg>z$aGn3|bsRQU(oGvGQ<`G0y>S8q`Rk5xPGeRuhMyQ{FYt2`b%iJ zboj93kQ-?i346%&lnV^y4_buhf#8l;8b;%o<%|HgI6y1&x%B4`TGS9aWvUStUyuR1 zeIg-P@y(wY+zPjGa7u`$?}g3Zjr?|3(p27iy3?U8htlaexNfi#P1v>1P5s$$dj*^} z!Bv9uw}3DW!sQk9`Gom9$RsVIeWp?(??V)svVyg;LSn+uuNql|;L@!O3{-rX&(T;6dl1_YxaJffY18)jM=T+<`(Sy~+Yz6A>-UPd zao4U%=)?Z$D$E(@`f*6}!0QN)91o8ZKo0x@r~+i)tlHp)a5=gzcaevYw)E?TsAZVi zo#Pcta1^i4J@AQ^F+~1o0xj*k6`d?)eOu`?D%})HN&yeZGW;9HzFH8FMOrR@>sCYV zh~-#Cz5FK^WJUn4saRVO0CL6pnNqT#-E?+tH+Yj_0Q6O~G16<#5GIKIUd!HK2~sEjRF9PmBoX>$aA zNkgQjQ^eo!MxN)vs-FLkI`ZQORI~Kr-w+5#G6=^?`>(Sp|0m}g%vh>`T9GB6p;|ZX zE@E^|(m;9LNi4*hihe6=V(LgCM@XEeUPjmCCUxbN8xS&X&SOQ^cYjGki#q$Fu zKbUF!)MB~w#X_hN!zgH#SY&`rImDZ=mo!VHKwLDB*EX|}QKj7bg-!Z$ibm!@{0 z%=7U4a5~1)_pI_&l)Z3x4Lyzok1|Ql6axRTG>Ndyg zx=*k|Cum8fD81t<;H0H4$WHqFwCf$rcm4o)H+AiNqL%R{rSfwi-_`?Al1`qBjUAut ze%8|m9XC5Pvu4ONgdj0X_wDij;{xFDsQVGmaNr=J5#0A$6ReB?Oo03=SINi(v5zSO z`hH`jk88rm+kaS=%zJ$qsa=5dZ+Uj6Y-CHfh*?;}lKkAj7)%Oe!j0b0U*}@jkaY!j^wbg0uJ6=2FHCA9 z)&BWGr{5^oK42M{VNT#=WB>YE>D9Gmz3=BuO<%DdHGK2){7mo#J41vu`Y%caNwX9u zw!ry5H5%m@Bo9D!GBSU4=bq?gvmL(~S{ngU0RkPv8q|wC*0_aZ_k@f+zP=7O^a%a6 z2pN#?Cn)~4?x95txg;DcEG!EWk||0UQIP@4>%j0gSCOt{YWyDVn18hZ*6NV@Vv{iK zJbW>LE~BZpw?d?RO(@^Gr)2tZmJaU%g75a^TW_3&ODD#_q}wqVXCbLB=qdj7uH~G_ zvC1hzOScvTDkoA~ZVGF&M%VEKjS(R+lzeQRnn~XS48~_aMI{$5*LN?vb@x{d?rW@J zNG2P+=g(|@A>s7l>%_c6^Hyfkhk`q`+{+q{!HgC{aYfEbX9EB}a;Js!N9|8HPsAMO zxK2^kh5+l`zOp#u@QepDGT?g|=8U5ehrZwY#;pc6&_ZBV=h`$C^x`H>af!(LyDf}f zf5PWLA*>wy+H;Q9sWvzV-6V#%^IXLeu`%>v$v0D!@Ry`4w|SI3s(F1aO=sgxkEwwD z=Bg;<`9K|b@>(D~$Ac6lS$`C7Op21MOtsvRDIi%GYMq=+-P|uWyUxPr3#}ytZT0mE zZti{9IzVCwq+ch?Um+D{F1%dQ?|Xs(JMG_Hajpl$$H01fEC~=+I5=8ywOIrpBKi{l z#fvqAUk^3)oJT7w%2&3!>vCn%wex6zAc(5Qv=i3SYAVp!>oFzW+%vc$bANrp`)B!9 z2cIF~>BG054UDUUOTp1Q)Rk{7pq9b$2^j3&Uzf|PgS5ktoX=LKihyMAKZ(}|3$!CYKZ9nALmrU&Jc4xK+D-`tl zMKqYfI!$c0+#9yP86np!3F2r^RoU3&l+O5ny<>=xm6pyL&>E&*n1oMt>{|7hKW&!a zrMi!xmQgh!h-YWSePF|T4JO@W1L}HcC_}zq#UEw2wlkD8w z3tl2|bV>+>3}W>gjvR8o0>HG>9E0iawrsBeBoUQ8s68EArp2wcJahgVL}q)^+t|Q) zLkGat755?sxh}p#AmQDF((&yZC4%3`*(>#3e`CohbX*^_a;$}-xcVLFhD(CbihO;{ zR$M{{mR|!7A)td;T7xYxKI2qwM1gH=!F<}Si$CB!YX%NX^^A?7m)Fy&4n88g?MQsQ zXWXSrKt@4wW(tb3>F@ng6;zLj7P>qlm80=?-Y-K((PyY#Pb;6QJk6m+iwU#_1bcpQ zTD1I#>3mgM>Q$r45s3y=70J_Zb;NXt6QSSL=y?`zN3P-ru$p3d4D%*rQx||4=X6QB z#LA^eW~gEb2YhqzY;+wIc#Y;NSl{CcwOK?!Sjie@Xm{?^h~ADck_r79Y)#^^S_ ziz+Z$XoW9aHoP{Vk{C{@occ!8Nsiuf$<=Ccv9Rm?l{%|9pqU6V@KeH6#S`KEvCmUY z-*&KGByn_e4ca?3KI-(O7EN=@^Vh{^#$Y$(K=j~>(s>1H2QjoTLx3lUEpW_qru=<( zgC*atFV^j+usiYXnbGef!tq=>S+S{OOf$;C zO$gWxa7WqI>{F2ef3YvKpqJvX+(%P=f&3g;q@B z+Emnzc!jTxx5+PF1X}BHQ$9e}_&ToGk#GBe8)jYA8OP^0`K{y((;H#FQ^<7-WZt%-FM(@oPn<87zh?wQ&ppRSY zboAnm_u5RBr;h|)16-N?(H_R|eXRR~$ozH!lu$@zJN_|;&(h@D@=D;?cT9gF9>xnX zB8@f$FCvZwnSy4GQ=f`#4=io`F6&W}IEf!b!2x|8*prV>74=x+03X>Ku*N_VMHfbt zTTG@o5NQhSnVC0|6KKFu9qi1p14J8gsVrWkzYq>R7jGC%xJLze3*j#=|J&pJ3BAOg z<+*Jh5>zr^FT&U=z4}F=J1078K}B{~TwKki;r<1^oj!D@x!79%9R1I}h;x56$Wrqg zlyFzP>~l2DQ+C5h22+RNISSveSb8X97*Ugu?zseU}Yjt6?l4d)UeZP zFE$oYyDbH_M+Y`}=|u7Yeq!_hOntiP9c)nk46I^dY1HN^>OAeVwRr1Nh+GQXm?4J7 z%7pZx=+4aB9RAXkIlXV#o(N9r>rPIdprheCtC!N^fngIDBY) zPSJQa!I+4|8n|7da@S%4R^jyS;QbWSsmEO=P~^Ji5x^qzI89|Y(NbYnZ`QDM?FAm& zInJmYVLd&{b%K`Ah`pbM>Spep;VPQRNgIy+$F@Yg%PfXxz9 zs;~!4mZ^@+-JuJZq{!YR0go~muze-xbF`MQ`oxCWy1*8bH&zvtltk(d1{bh!lCU+y zZ;I-3om`?t0L<9>5(&US-VuKj(R(kWZoUB?_$F0`Tm3!&3D)rYc!jXwLsGB5bPG^; ztb8!K&_Z=&DFBrc zr620FV-6+uJ?rZg9v@N@y?o&1GoZiV${8UyIT_I@KzuA}*P%<|tzy3N5luh2_a+VZ&eTYF|<+fx6AGB-_=g>BsS zLnaJ^VlXhhgnG`$5W*}TuVw!wKuSb`*hc@5PFe1aZJH2tbZ!?AW)nAbnKmyUHrg}y zZWlID*qDT-%$p!>%PGrsbA5@#&|Cg8S(CC!toJLKhw>Agv}?W6w< zIv8~nPLV(O*J&wZAw%&b6y~v_ab;uqjo$mwlJw*gc=r1Q0<3|WEfm37rKOCxb#t!L zO#VjM3aJ6tS$V|n5RpGH6`&ma9&!Io>NVnebAWGeW%<@Rsi)@JhAZfXH3^@B?tGQcxG{{Dg>V!Bvip&N!$2+Re2 zUnWF>WG5c)$|-Sr=`)GhDx55&L-M5xsnV$}*R$yS9;U>4`KWLPsg+0^15gF^QNR5O zyt%%IY(S@M5P(1QgkauX<%L`mFcLE}i6&t1;sXwI@L;4Djt&%T1@PEt#r~ZADk;TF_owFto7zo{HIuuQDEq=2m0&#ADj|z{>Qp<3Ys0ED5_X zQtlD!7%XlPe$@qLO{((8AH~HI^@qHE8=*E|s+_XElH7DwNa*w9gW9XDsa~*j=L(w2 zmIe#~4B1K1h?nC%viTvmDx%-aRQUKDgO(>FuAVj0k zfujgMBFad(>uJi7nWFdX!D}A_dMq`psKr53ryW`4@z;MHyo7|BwA42$pbef7m z-U(Yx8aojXPei##FN@K$fqRYGf}RddBqFsZmNM0Lp~7o$`!>D3mZl=NW?OJIjf%kt z%^LPfSkpuJhh7w?1Yneq>f*Zlku|KcDWhoMRWiikI9g*&0S#hhpaZJ)?kiM+xXa@4 zv{-$2Sn69Ww_&e|?M4Lh6*lA*9wWK(xN7GpF{LzBldfJY0#}eB?!N*g9o|%cBd!>;1B6!{Ue)gjvtPj0VnY}O=h?yECB@AJU5RJZH`bW}{>eP5V1GG*K8-xcY; z?BCPgXi#ZNN%zgto15k$oTSRu+d2@~=Ytaw5YsJhH$H1?!>^My;o+>|MJYQ&i763Q z`TlAd;A!FU1BoA#ARcsu)Dt1qkHJU;TdTdjX6x^hE`lYc`-;t24ZAmac0r(y0(Uul z+B&VcbYC+)-GTxUK>ln=cC6=yTVzZL0ZfKhia9UY!5;YaEd)VNE*)Rul+u`w$HBaP z#VE&Lzs{U6 z3;W_hgzUAUn{Z@=RLa@b3mP9E?5ly4)ll6lQX_+)kr;S`(2t{!=bp!$Md-7d6Kz%$ z+99+eKz3m&d5}vY5oUE*3g2XlZ~dLwl9tn>Zp%os+ehPO9e;cR1zHj;@D`WdZgUv8 ziRV5ao&@!i4FfcpU*{%0Mc~2CrKLZe+5Z}^^MTwX--$cO5WI?buYuR#w)Z-{SmPtd z))UGe?B@QgHOn6?x}(BLEgU<@P$JttAz0TSgD)QDny$hLVr=dm-zezdnL|&*^F6>A zQ#A~qa0e?5HNTU6WUwGS#cmZRRmHI3##|&152grEbzy*l!=TL~*n4|!+aCzOlTsR) z*Y-}347t0kj^ZxiK~GWAJxUM+G=(e_z*Kk>pnTWK;;;rG4(zuoxM)L$lv#^fYmd!0 zrWrUn0kwOkoe{U zH*d{-fB0IGL1iJ1F7#~w+v3VuF9A2hh6|RvsQlP9T)Sqdy;`rYFtJ4YO7YxN=v6!# z6lsVZ96!GZ{9lRxJ!gcKDAv=i7Xq8!YDaO+=Eede4%uYI%zGKV zAVb=xofE-%sLA%{t%_Wn0*jLq4HhXNA|Z=Va5kmVpz$av>%42bNEtg>tTT>Y9F=vZ z7qPm$!5xWpmcY53Pl;O--oHe=7gkWpuYD+RX=xGohJrW0VODf+kV?U-0yew3ZQYgx)5mZl~|82 z(c&X1nC(Jfq00&S-ki}gN#-l2ro8jN!V@)Da+Y1KAiKbl0r)oNGOmhAt$Adyor?c~ zK(X5ys)!rsnr==J1GKD=dN(?G&J_E2OazSH!btP&P*ZXiBiB76p0d4ckVs%-`A(vP&CX)Bqq*X0bO0xAv9N33D48cE)2X zc}P`Kk+X_YOQXWvh+zsx01gCg+ADg2_$=NV1$;y$CJEX&oI}~az5%J?t1a(&pA^7w zzWW6IVsoQGM8))2LP>?suF__4;E^bTfYqCY*k~L{k>=zmzN8fWgyRd_QB5+x7*dVm zVLoyc!1{WMUK<|1kH5)pzKL|Y;?ywVaq+;_-M30 z;1bT=f_&`U@eeM2)4Zme^+X~v2;>mKdX1>(Uq3C4I7^%vZRrncO%OjT+tF|FqFx+K zmHM1DT za~~l0JBn=`l)1Zhy;@DhGDcczC9PvhgEQs`x-3Kif9utGhKNaMEDS!~z{rVP2Z9`E zZfF|vt?q`#B-p;8X@kxPyoG|Gr~eWAyU-#^;~oEF76b!a#A}xd52KWaqo`#11 z@KSe2gfhl#{3!q!*aV>+#o`0-IEzfeQ1VGn`fQh!@y4#M+WFz^vuEE!I&lSAVG*1L z!c#~vf-()9`2VWBA#{U@3@+~b#nozweCz0zlz(QGQrX93viDqp?Uyuf>T8`J3*DBN zQt})Oq)vV7?cIH8y{Vw9?5`aF0!-`3QqL&@S}TXjV}YA2Q|hm6wtdbxabB zl0-(*u-?k}8c^bLqE}5Pm{#?%+On*uO)$U7Wj zh7b(ANKY&O=z2=9hRb>_CaceCl5j`-QOQu8oH{?hQTZwsY6$4xFmc~tS-bKkwUXn3 zeG#MtuT|22E>uDk%`Zf4I19~uz3-Ih^yZISDY#iYwns{W8Dwj zn!y4_?iZB1oJ_$8$Pr1a2vCj)2tRY{K38;;q{EEOcT4shh7c3&{C6$66gOURJY~JU zbNA)mv68us@s16d@(xzg2RltHFEFm8KHWpp-_~KN;=Qr4;iXRbF7tF}=k=opI9a~_ z^MjD#*JO(4ZS||0yU}I!#x3p&W2qk2`T*f!eQq=Ln^I~- z4E>FLQL$9xQK`ziv}7r1Z5E4*y&7lB7&RlKUueK58-Q5yae21e~-L!MhWOCWmyKMF|QKyj;%)&CWxVU;)%BQC0V0-(J z|0{UvSTDV+iEmh=3G@-Xt{MD)TmTJj^<(lv$6o&Pzil{_xmyzUpTp{KTJS+=9;5oB zl$kHx-d!L*_1+)rFTQ4koX^@GTRx0z#>TJXmzUqHupfEKh9%ydvp0SL|xf6A- zTcp5%b?GBS<>c`8d%1^d$-|-pe+Ztbb_1R)f+G6VoyMAHU^JIIS#FiT`(ZRA@RLZ^T44bo2;~lfgfp8=hMpv+?83Dzya0!yKN@n z@X&qs#At7k2^)I{G$=5uI=S9xrm3GeQvSDK`)PxjiL2|2+1U-?lP49aTJ6K8AZ2+F z5a48Lb`9!1I+|m~d8hR}?81nis${OE!lPp>h$}b}RQc-J3Oog>s)}K9gha#Uwh_h3 z#;S%YyZj}Ar<*I=TWo{(9wV67uEaX_yOp>&)PX8-s>gWW!Rk%a%i}VSM zi+Z0)E|r2jmrXr7to-h2n%Xu8?)s$xa;SWv!s_+ws2b0%f1$=D6;{rfKl?5Of@9nUO>?ra$_yNoZA3x6;ndVZ zQ|~m>(VE}OJ%PaSw&aOfN9cY)2{9(orJlE27-=WC)U)mI0G$SKlVXQi^mmH7=tYh`EdzhKtDD5#d z-I*TvKKX0r=!($pYQr2d9qaCyN&CFwn9f#S2GZ9^I_%+&u+LiD(7kzUUsvaKW>Tx8 z`xz(%Z~{c>#eA8`*IRf|IB`GLTko$oK?|%D8~07y>Ta!vd1g^N3O8mwwR;2U=;Yi> zzTW9~bVx;77U}EOSk^F=WI~&)ZT2ndnA2%h(^l7Y8;A!=&`inaDZ-tcKacG3C|LibKaPz}ybG$2Z?R!djC`%2*SAE=K~4(LS)iIGt3k(SuW! zGYptOWh)3GJE%GaYR|-tHT0q|M!vltF=q-GIE(5^$hUn&DL;FAv5 z1tWX=c!RS6tTpKRu%rHTa1Zyt5YpzL$3KNcJLTXOXw8!({@YO;Y1<2=<9#l3!)6W~ zu5NceHE7Mso)tNdn%*Yge%re8VJw8)f6!=$UAP(P-1o22d z^30FN z5usNcKb?D!P5HXTPhN^wUFwrlx~hqlKGmMhtv7TtU*tX3TSDgBvJ*ZJpXgn(%t`^n zera-i`eRY3nz`|}GbYz(X5{hEkB)s!@9ghRe`Y08kzQ1E82;TE(NXNTg00Vg?=j!= zFU{QIqSC2bY|5_+Is#kn-myD}8&zr5KD;3Iw#_)bEoQw~PU;IBFPFNBI*AM+PU`)) z%gmnoXBvn19|(Kh%e~gW^27G%UZ>)jJ(9BNDB`pML$?>-W(9nz`}ozZZ)psgJ#kQiC1>b80eaIJwtR04>gtp~5 zI3v70VM0eIi#H;m+5ww$YBXla$qXf^>TC2mMMt~UNOqg(2p_FM1|KcDIMUw32n1n4 z-ow?TccUSinbM$&!#lu9`;_eA-Lu7({~`PJyBvK60sY^N1Bd1Y{^&e_f%}f_tpRUY zhEPf=AK!f?u*Snv%e~i^5h;s(HluJTF1HlcPhq~3!@(SZC`R+Qh!oP$dB$AJ@99~_ z5bpsChv@%>pXz@vp+alU7x7B3Z^A*)Hk~zL_digDj!9$pmPJqh4_g&2v1| z;!pe4B=q#Mp6eYYUC8ZjV!TvI|C4r$!Kfi2GdTz+6Pj5iZng1=`ShK23W%I`s@v-R zoAyg&@;V!N&#Yxh-)nJU28so?JCdoC=yVJW4H;z9(~`3?GkeBP=rzEd&T_L&-;>GoGeZ4&d6s>4sw4Tz>owF6N8t;jNYzuwhDIx+V7kl zpWON4Q)a>Pe0bu`y!^e_mOlA6R1DnuU&7M0UAN;7!9d@n!RY+n6-I1|7aK;Xj%(Gv{T z<#$YDn%-8ALu6j68Ee+;xZwDfF18-c%~(nlZ%5wo zz|Ga_DA*qR4OeeW%=S-mP)dbv%+e9_+s7J{4lTsbZHf^OWLwUip(fGj@991i%n^Cs zqH$x(J^LUI1)BiIa{%k!3a?cSi&a&ti`u_-k=X&J-Pwzog=WCK;BBBt1*PR~Upb@c zlA;vY`eVXyU;Pv1U9s-=5VNuqcC8!BYj-Vjm3Y-v&z%juEg3Ua`H7N!8K)mg1MyQ9 z@O&zy$?N)*WAqmmT?cT8lOIzPb9`%dLl!pV%6l&RUV#HXv|SK4(FEGnYFHY@Vy**u zROD|TV#kRa91=ITkMtJ`@9)+Rv-tRTdFM-i!N5R83|tsJAyhiSxSa8$X=e8)WH!;( z!@B71UAx?}1Z)t#^_N;NF~{ttc%l?{s+J$|I5Lc46~Etm2OhRMJB|sVVpt4x=sWf|f2 zch6|KFvJLpb8YIx3Y^RaXWtq#>&3Q#EXeT)ggzb zzZ0}FK)nBs;M`stlil845;#{m*%b~TA_DQDQ&v{2VxDdZsEVp0Zxm8R?~aXrK?cEb zS_?!6po>K-^7X41Xd4i6J1P4yLWJUDaJD{x(%szBA&t3jDftKT&{kK;;6MZGd!JqL z4_=OPAV7ju_Q(o^v6J>TA2tDTkoi50AxzwRaj)q41z99~%UBXVCKz50rncnHXe<)4%Mk!f@gzjs_F~S2LomJAkLG6jxsMAJG9k0Q%~`~h zz4;Zo|JQSod_v%kc`HJA@ohzB4A)IChN0#eIJ00BZDm+J z=bpU)yree0j^W~39ce$X7cK^l4-gPdf9TTYE%2~xlUr2h*NPE4Wz2s^Vy)&D3`P1- zG9cX^d~%6CHLoS}am#snQ=#fc*l&szU4aL+@5$a06GxszJ^_K`k3S{^DM?>{8=;cv z{c`pA?LS%0Egra7YrSK$!nBRc^1U8{coT_K2@{jGB zD>8d=a?>CVsy8~@I=iuWUlFQv2yglOx4y34FFJaEbMG`Rm6GnQA|2V%!YEcLlIO2! zUvc~=M-48*iM3)0zmbvSx!f%+4i1$kNnq*&ywL8E#B%2#PwgckYNiOzmmO1}%Hx#<=ro3c5+?wQ&V_$)X-L*Y|U z&~tIMG6p=JIKSS1q#wR^((Q%$B3Qyu8Z&rwERlR0dG*Ua_5N&8`F(w9At}YKfzJkE zz(<%!G4^~L`Huc|Vrro@qxRk4JU3K0n5*Z+D)F#R`|;^Axmg2mUQQ^Kv$nMLWk>!$uD%1D>wf*eq)CNjOCiaw z$V`!uk?fIKvS;>+q7t%KR+11x_9h8Q$j;8nN|KO}-|O2s=eqva-*v9*JkL4L6W@=| z`+eW{>wXQH)Pj$}i{Z!j%{T=X{uvm5(h@5Oqznl$SaohCv`znuSjR)bkPhtF`L^n_ z)cVV=rLq^8_!FUl4T+A%Y8c!y;lIPS^aWgo%Sgp|tULTTO;tmGbpdw(T9#Oz9Gspm z_(gBqd2g?68x1xcbNiz5w{QE;_J5Q7v}pc!YA9W0Tuc4S%lSAt&#Zo5*?vwvOYO^k z006+u_f=TP0{nUQaIm!abpJi{#OC}8%;AF$621Ed?KT~SK6YE4#lHszok-w}gU;O4 ze)o^2CPklao^j~Y_k?*+9G(^aHEc_dhY>imvUYi)xEnQO53(~i%8EaKbQk`2Q0hAt z78OaI*PX&*N4&UyHC*=4Qk!TH8o-%O{9&w!3JRKnSbgvSwH2bKI{0&eMF{(I}6vdXr8pko~EQcL-r-N^5K&w+Ge-UAgOaOs;QU}1uHH= zjllvwaQHypwIpbv1&%5|2*whi`9+2{mvJ%2#B&=Po}cRl@mqZ~{#RLlBFc-Kr(pQU z)?%{S_I5|8?a@HoqQ{i!;^-wu<}4YM2kvawT@?BNX(~7}I7XXa_Da~J{VS7mFH_kMK!%h_^xa>)D74zsPEQCS46s7SSpvX?Z9(?}vLl}rs#y?G5vLfG zJ%23j;_Uu=Vf61a4=&rLIm^WUelnCXJoiy&((1ZSRVRg=SN!Owc5t-h>|4Bdcbvj{ zUqBz;K>}fDR+(thVR`$tZtohrh_tT7TDFD7M-H{_!uqGAg#IynzDl0Bt z+1uqJ8u?vDpYqMFk*KNdOwaIR5bdqaUa@@S)hENLcMT~3zbqQ$Jp1%7>)L(=IsthR z;NYA2z6QLVgY&7`Ex~p6&bs_`A&4dh2Sz6bfuab<9dS&kQXMMjdQ_;|8KFp}es5Lg z`ExFXCyIlyl5vWr@@WeRaSssV2M_jPPkZlP%$n1g z<+c4Ph^h^S!~&dtk*{=owdLtppbK-Kb&5IAI^#QpU13jcLb4J|ATx+RxZ6>wvwPledeP{_ z^cQCA28?Zx2R?8X&3sAbaF+1*w}D|{s28aGG+hX0 z$Za96tRD`jfux5^?J6JR@!<2L1B7q=*;yxW&`%f6&e#{)B`53JEnwWyMBKSu)fqro zZzuBe@w(ztAN{*fS;`$_4xMvP$rw&i>7#R^C*%a6o^z!+W}8vm6VO*#Ue$zZ4zv^- zGyd+9g)=w!*wYU8c?;&rQc9!sO9|_AOK8@!$=xGe<+aj7`_uiTZmw$PS+SwEOf^SV z3MvS|zJ47^*Q}Jxe{v#LA-ugwE~D>JLz_zW?)8lAU!^n*Req8ZwEZuW!! zoCzZz#{NZD82R}_Cxd&j=j~JVtd}t*S5y!B8y9jGsD^hhIOSZz-HcxYYoz?3%LE(D z+;dA~IE6j2i}`Ar7L}nHi5F%cQ9@#)OJ%_9>~WxYV>h;Q8H&-IUSUlgc~<@uQ?+Be z{&UrEVDFLP;n{-w39TLJtDajLzv*db-@vkeV;yf7`gEiy7aUii>@RJv zvh#J&qkVn->gR<((Y|Rlx+>120YKZk-v_oS9UdMwz)A#{I2ieW{o-6lgk^oUKgG1W zuk^sH>KmD#-y(Oyzr?kPSf>A$EoJ1ZLTA=F%Zw%PE6uLT+L=Z60xkf?*M6Sz`3b4? zGDBX?9}ds8Lmk~kcf|F}^-eM6>&lH<+#atk^kxZqgs2Pmc1bWxfded6O_XZS41 zQ0|Usu9~Z4o`edS>(Wb}IGz zZzNMB9SbiX&BW`oyi9)J!2#bL3k$Y59H@>@J?5%J#686K^%GVyGV-kq&+tBjm-1! z`hL(IJXebx#^3EwQ=Fs4vp^cPhn*D zV$9UbRlFAJDyx;!@zP7ssyKdbNAyNLl|X3&x0mXphf#@S|@k6DgxGR|(I zn>=r0RoH*YDmw@yXtAdd%E8*>%~Sz073IBHV)}MLghzD+2B0P zSdg?hC+IbjT*uMS%n7DJH)(&;%;Ms7z^P+q_CKh;t!7WZuorr-cI{3i@EOCc52K&E z0A>HykohlqcU~pCIV<61^;T>t>E!5U0xuDj_k`fzGYGs18yPu)7#!YnLPD1m6iD#v zL<6wAd;`tI#CU@M;1I%gN>PIRkr-hCQRg>2yA)ppEw%$xl zD`39nwk}3zXLa<0)v`|?dWq>0?G@|@I|k+^^P57D4yvlp8vL~vT^x*Jy=r{BEK>5? z;Wo@lsd`;EQqJUK&wOz1B}Ai-aO1}%4t@Dvxi)71B!959f|UMh z?XgfB*)b{FS@j{C^9dHReL9zkNgt@*9si_n5Yj`!7DW_5D7WS>gL&`Fo<{gcggV5X zOVN3obRc`yj<$dKa(umNw@%5Kvz@@z;p?97YDbUagy{z$4z^+?`WeGOB;CHoDc~w6 zBQBc=IT6CNIC{oVIYVtAF}h0JWD7js`T6}T!a#_0WApm0vB*@`8xAJ&oW_n%xjb8CduK%)!aZ0M#Py3+ReP(OL{1h8uiynQ3*8d4i>!W>3)y)*Xj zE>lM)t=#(+;D;Y55Q14=@PCz5z5f~K@}RN~nWGt7)*9b4n+6Rt8 zl(w~BpW(LQtGqlu5fAD+cVuU0`>%;7Pfx`r-?FuhxaKv^dZ?+@P(4|=@O@wHU$z~H zz=SE)F?6I>d9B^jI-Itp!FVeH(6h(|e)dEXNlS|gA`zgPCf6uCo3Mh>Z6{H2{9{Nj z6SxaXVlbP&?~dyHlBh(@P8lT`+1%`$o=$`rcndj}O^>sdkf65IQ?1(anJ7*RHP9abU0Hqu0Npg8L z7~447fuVl3PPciG2_e`Y4~ocU`^&B=AYmu*sOI$AvRBMH1*Y;-%HQCh10)+Ee|kD> z&~CK;{gu6j8w2x+q8U-ifPD#zZo}=B!SmJa1Mv=t8(xP*M21lO!(Y&WqQQsH3db&7 zeegr`zBc^@g%6B-Z1Zpx3%mb(6Jq>nFb53gjMVyIuFD*0lQtX^Q?NByE|HAuvodoR zzRtV$+J@pCmX5WxnNO2qS;(&Rws0%k+48R@W8Gzo@DjCmi>}q#`{I>2CBLmq2cfV5 zS%DW6!pt_1Z3v_W7Z_e{fxLo@rb=VSa;_E}(lRgi%Z$k*wY6%_P4iA4+?50T%G{}c z*jkkBI1&Jp+o4appS0uNOJxvsd8}(U++-Yj;Vm-iXiS$kPAOXwLWDLQp8Y4$jxNFc zu`*kvyY=}lPmc4yrI%xU*0qoTnflqfFO+!BZ9WGkNyi$e9U<5Tu{0Wo9^DJV!pBzz z5azl#H`6&Qg$?kpmGv-_Dal&xSGd8@EtAj9O9Ro)5a~=*Iw{ME+ZpkPCExYJ2r%2+ zPlCfIPxhfXWCFH(@59W{JO-8HeEn;$D>bY9#Bg>&zz~ z(WwZ$m6p~bBQFo;m>(Kte8FVm9a5&#v=YGo;8O4FKMay1Avy&6e2{WqJcEil#lFl#N=ms$4w<3X{Fgj^2n|hh1=a)>oX#j&l-Ne&1Qz`;pPT_8PFa#p+s{}^j5=dja>E?i9+$;S{c z1RKDBm(6IIIW2yLXRGdh4>$Ba29|C+1wFS-Ii zvhtp3eVkWI{YtYTJgJ!ytAT}?$;1Sb$d>5xsTkXeN>To?YXq$1vh;tu$r2;J8Q}6N z92&Fp8~sW8K7;U_%>K2qFvz@v2n(BeEXg+6>g$sTO^Kf7W0@__HtEiJ=c1B|nS~WJ zerS}j*rTF)3$Fq_o@@@=eZF_OS|%M#z`NPwXTZ3v7AW9CB_X8(up41TK!LC#0@+~R z%JkL?IB;W`3(K*HE0aAPdde~eYu_w}HM`A?isQi%V}qj>yE=O;fuoP7h!+q`wLcpP zJ>fUfD5Y)-)Ff2SnU+qYd;-?KB5p|7Mef>LVPEbtU2rbp;6LhFJ(jug@myltn_bWx zBqI|3;@DKI;k#Q7&->uh?Kozy{oXV$WWR0>TY9?Y`p?+fcQ7>&r6CoaHWFd5ecZ@~ zDAqyPkL;>6Xp&6%2oBt+2BUQH_U0D;WKiC?csN?SU?ql3XTQTEl4;DDC-;EXl%(W; zoISYlu$VnbsF=ee)MKug`bF2VWnuL*>a<7Op{Cw>aWDZP03Yuxwwc}={mIXddfBn^{cT1JyX+Vg#ngV!_$iv6%( z7XNYYo=wx}Xs}-HJ6!r^*xim8FxjrHmfHYxTz8iy18t=;<&*hYcvFz5b9BZq$5|FP z#+?gNRD8wK5~wCZqj;m3?o8aTD-jV)zngqES@6&cxFD=S+~}d@_bmP;UYZ=y@FgZ= zi@cXAyRpyzob#!~aD11mSI?)XpI49lVoU6z6N9O8?9|0L$%tE+=DmKU`oA9ZDdFn7 zezp9>7pHh~Vv|=Q6wMT*Y>3fIxIf;gZI_z1;jv^uM%=?lb0{M~ssQBe4<`F{Vl+~^ z5y7+mb5EwaL2Knu03r#ZIZu2v-F5jg!8FBtb3%^c(79uwNtVSAK^DMOkL)sbs%+4G z^0i4p1T5WWfe%2M4(DNg z#i7yK0sEyWA(e?pbQ}@Kuqd482^{9C;~b9V%*;_XQL~Jb}zIslbPnncDeT zFy}7svaSg?o41qxAFR7OjxU z(m46W;P&=ulRX%SKmp0R++nTx^E)^(jMUuGQTb!Z8Oqk@>1k*!rn@uUG0}lR=`d)s z1+t7N!o5lGLaeM16QrfoHZ}{Jjdor!M^1geY5k2IRy%!salfkmdfY|g3AztB`*<1L zw)^|T|Ju#Mm9eFw>Up#Ng{z1LrNX|gJvZq*@Z|uetr_MW;1K&Z+(g@|#-jiT>`9^? zbQY+&D9F1(5mXC0092-Qw8sCM_fHzx&{dG-E>T9S{50ZwfR?1ZA1fW-+1VXdR<|RX zi9QL*=&y7hGO?~!Q7p6@UFCk$2U|b*;CMU$qfph=Vb<;U*cq7CY*US~ByeL3@Met9 zIG?8HVHey{${i8+_GW2f*I*z=CEAvcc+J0!<~_DEOgQ1n9eid(QRep4$Q4qo-mhO} zi&Y!Cv&S#DhDX}K+Z|qea6m&&2SHIbD}_#qIf(}ehv=-4X?il2GhF^?7;7B7bi~US z%%~0=#mI}fXGODNXZdTg6S16U&OR@KA=Pe-&;SI`dDwn_t)C+g6%ggb1n1oda`*NN zEOR28T>JyY7mi6kvbJ(m$&m+{gE{=M$f02}SPytsld&pj#hMK|kX8=s*_B@H8*B>( zr?$#~evjXwY6C4^mS$tVN1`D!=X>v1a)7v+n$y~VaY&ns4U13=XelY@F`c1;LQZ08 zDhb3JXqN8%26^e5mX>~aiG6pV>0cS@ODZ+ad@=MJFtT=zV4tsd;b-f>ABoMB^E_`_ zn>MktkYYBZ4tMQK&-wo*9|^Mi6?fD0CTn7s@(o$Jxw)%;kHc61WazbSdP-xlzrSPG zW4C`U7nO|Y^}lI7E+>Hb5<>ydDm1bfFEBt7OOV@R7~{#fWf7{gAz;2*@ze2RMQS7MEAN&gpJ@>85g&sJaJsg+RpB# zF-rho1Z8$~D_C;$CW%tvTQ)o&r+B5#is`?#HDrzJtF=G=8PEb}yGQ_Qq-X4c_m6!& z$&!_Y#cli9PRL>7F>R>LiO>hjC?%FfA}BA7p6b%gvIriYQ9f7mRcJA;w znbgRZ*lV|&7@5k(mZ3t!(arOk(vduo#y1g}EGC1Y9SDGTZP42*7Ee zjn+L=lNHHpXS9wIz-9%UbmtS7Xbike_!q^WaidWxdZ3cL@buLK@ct4V! z2rv!65SNdih+*mus0M9+$OG1ch$BR1AmhEm#Hx5j`LUCLGYaC}Fz%e4b^mg2%aZ&< zF zTw+VTB@`0uY$Mv3+!77dA)?cbaWpTiJ+}XC-`?J^by7~CC+;)Z@q>FV078aeI%Y?Z zals`NdSy*rI(WVPs77MOg$pA#u?^PezpM$82bnZaeR!f6$Wl?>k2_O4M+Qash3bO} zHM@-!a2YL4%~>N=;KLQ`xMEC6awynPVuaV9eEKm4=GfsnD+ z5yii~gKYaMVDNGylH|2!BpBqoP{?R4w$nZ?{^OLo`E(Fp`p2TPLBh-mR|*B1%gxSU ziaT(lom}Y~9UC0|D>B_)+wk@+rQz?lwKvVpXYOq2rK&Vx+EbyN-+jOq??=FWhw!Q@ z1~pkeX%$>pa9PAJ1Fe)xbrSv5@%ec5*nDfzk2b}7o4r3UdJ(^dmbda2HepK6iO07TI5qDw{dYp1oyyN_seV?+=m;)JeD9TY}3Hb-MyuI9fiJL~U1 zIL{-%IQn<>@XS2@;EIC?!&ODasy57L1ylJT?NEMzCD;wTUz=X{pYaYQN|M?@l$Qqy85>uY(8b50$_r^3Y(J3$8yG9KB)hkg#Fw0p`l>!(67+F=Nciqh zFI5{%GX1UB1v3rU_D#Hqv;FZMY%TH%b~&08pYwN~jib1CDNst1cD738*r=`BQUh!( zNP+$z&hBx#Y?jQ$Y1|}|gj{mhrs+D3dys>XA4o$DbH_~u_#V7GLNc4c&rg>(H0liU zsMi|>N8ZjC8sxxMyRi0wtj>9N6%Vl%!O0%v>&F!I;hk8#AYa(G6mhN~D00f6u7 zvNwFTO*3qS62E3*I)(V8_Ua6kuuXSI6bf7QA}A0}};~EE;s+ zFSzE0O;^)D3x zwT~Mu=sLaSWnCiH*6eS8+b+xdP`%PT0(_QR&Vm8_^_bv;X?jyKA#(o?iJ|`gnkHQo zB-Bo{ziN>y({tbd3<9|N;!?V7Rl9l{w46}K?X9(r?q7cmDQQVPz?Sc~4i7mbh*k?b zDMm(E79DlA<_Uk_BWtBkNL!?BxKT71q$HPxWHm7iiZd(BpbZc*x_7mIBz~!B1z`c7 zvGMQ=Rd?0XCLf&~9D)_ok&|kA47dA^NApVqGr6U!=zN0w4Q_+%-{l*%AA#`iy zIu^X#lpJ%9jM$^t$gXsZe6P4++SmH2Ya&%O{7KTmOo_k2&jVa1Fh1O9e@gzUHzzmI zak}kBvB&HFD2izG#B)TF;czz9@$gt3?RMBh6zR1&5lZx-Mnx}~9Cg@|77#Z=O^`LM z)yThCX8Z_SVsxj`Qp|gw1#%^?f877Q-0VI<=RkI4Y=y%FF<~L;Z1iXxEG2Nr;d_EX z4Il>K4tBHlTxBlJ9~=768hDfLNs4Eo4!EUl7>ZW}p|!9xL#f4JaQ=J*7nc+$RcIg} zsgQPlRfJ|?THN&_0IuV5AH>5=Xc!kh7eKci4*Cq*_K;%>C+S+dmw!4{j`)azdpEk? zCW>n|gkiS9cC=<^8O#IBOxdm=hkJ2kSYV8ef^atYY>T)-83R56eUOO$J0s;@{fr~f;sA)J zWl&EX#JGTzLoeYs=zLV=P8$YFMKLshvpcgm%EiaaB?V9M-(IU%L*5~?5^58vR2_^X zdk?Yi77}#y3VePEI3cWmJI1_6?P3C+Q!G9!+abMTw+sYAQbM?KR3SJ0KRi>P{~Zrm zCy{$AD^kPKPR7$8M^8ctWN}))_`0#tGxw&U%C#Vea{05Fk6Q&AKjItd>mS2U4>5~+ z)+G)NBa@SXt)`#|8}ZM%oGdaelz!^%k%$L(sd}YHfi>cWG0T2-cN_fU_wG^Y@dU*i z7o(N>x2f{Q0*CUR!}5dd`WrW#HJU@MtQ?`|M0Joj8<%lC=k}C52;7gcVFj`+Hk3;h z#^1Q)Z7^XE0uJ`I7~pmE9`}+c*nT^%#t*6+@qn>R-5#WwtwX(-I$X*Dv?Wk(d8!(&J&lfK-ZBHBqe250& zixFqRE1}HB#;E6!U>Ad~1SawcXloYilIu**5u&CsW5>3nf8~vD-Gj{ul1!UHdeo~Z zA`lpEoXCoyz^>-fQRBRl*Br*Luz~oqp$iz&?`Hm6;8%pKz^(@#jU&E)wrb~dPyFq_ zSh+V(iyTs;3lkH8@7;bFt;H-pVLzZ(_BxQY2a|wr9Y1icbhg2C$*?sT>WnpAtWh+} zfOW^_O2<&rZZquKKVJ!JiJp&h5)EQOj#RASQyj!Dgau@`)Tb%PU$#T(^*<)^y4mJQ zGbNS+PoJx2K@H`;-0E&yU3@sXBKJOp!9nFl*{N;Co3SNR9i3#XDvvS5FWVv)qdaG16f? z&?Hy8V8blrY745{?Zsgi-R<-V1z}fK5CE}2e8?X7zThK{Nv^EFfXJ~#opHLc!a5T-Qnf<B!*dYxR|d+qR_i?MeL|q*!!%q{lFnFI=@JC< zfM92cWRYHXM!4L7TCLR82^`~Z*85^xT9(@{1|Q$&(QpFT=Voi0t5oT4)~fACP%ms zBw&5A=aOe-LgnH}(}gwC<YJ5KcuP{koAv+jcd= zMMRBvzb@`+wkcg9e7Exf)<6z0a5dbFetL|_WuuoUDvs7q)j zFf!D!fyB9l!GVZAYcxlg5;bHZf=(Ik)gc}sOvFT)9xR1uHVH&V&`mP0;9R0!KEsiQ zlrmD0Bd6YVSm1uE%ZfJGwJYQ|7+F|8Jn_kYN%@++Joul+aAn;)onE@fGUr&Z(NF~U zZ|~!QAIf2ZU<_)KI7mJDgJI6E_DuB~D*v@9LV^ zc)(8s;V(K#P`2BY)=H>jfc*nECrDSLNlRot*Y;a~9*>AH0ewej9D)P2O!fx{Loc{U zG0qdGF}x`5k%p>7FSnOoAt&Ky?k^8=3FQ$xa61k1SRzk@cz?^%a#xblyJ@wv*o>XS zW*t@#(Ek?~vw{*-@9&8>E?kj=ItdV*)cgaI;7RQ z+QQHo6!!y=5$V2Tz{@C_T|`Slg*&x2vp@hh$Ovf#y_(b0Ebj7adUk%i`t)*K7{ z!o-WUlF$WNay$$9^S9DxO+|cbb^@R^8pOO>?+0mV>`)UzubvzVqcD%^T!VKL7>i%T zT+_};zuJVfa3pCQy+w;p4Y-!9(A`Y-$=Tk#C_Qa18D~*~8e)CB-*cH=^w1y0jm*kL z-O-UX{bBB6O{@Hq?sWww^sjX&_StKj5{)Td`T5{u;&S+yDE!6l-{<2yrPxOLk(gnh z5xudob=CV4)|P@>a=kUbYFtOt7Hje}Mwy%|UzAjw?0?_5eAE()qzw+) zt-{@y?gw2?Gm49S_~oR1y8VaSt3G|rd~N8A1EK#*sjL2GHdTNuSTz{l2C>ThTIuQP zQfpdYcI@sJ!ma^9?tXQzH)+H@U1GqgKYrZIqH#jU(vo3w%FrA6Tafgklen2rYFj#$ zHUDNvk20C}AtWI(nn_CMrrX;w*v@==rP4-UZ2+B|=KYJoGDeFd5-{qt%R-)JM)Fv% z?xJ=I`^)#`j&`rj+J^;;64NL`)7e5P?u0c@>gXvV6$&ynPLU~Mi8q!46bI5iSqj{* zKP`R9z~f53i;Z<(x6c2#0I8ikUaQ|2VtLlJuMg+uUYCB{HXZb#K3KqN4y`gV(SAf_ zV8wvQW<+66r3*#FhD6-3%3*7BrSY{LWp>XaxT(WJD%g~a7-TZHKE_43Kyq+wdnoU| zbQE9FRyWVi#wF^pVsF#GKEk`}bgVQR8x>T{MMbZM#MhMyyM<^&%?LklMe2@*;LcB9 z8prrf8&?+aku_QOBJr(xT0v}WNH~r3rv2-HYR|qL_N?ZqHjy+Q%65KGH%f$W0k zj>zQofqI1C1@mG$_;{QM4`UR8VH}W*z%SW72k=o&+GW+&?gZ-;WbDGiJ&5fV;)1lR zq6lol^uof2U%zr}{uM${SVzYW&Frzhwo~WmoL}h=zJrN5us{Tnz;FM&i8XIzGZIU7 z5dV_}E3ikU=jR9M+V(fBE>zCW4cu>L72Ug7mP!p_)v)c0l3pZvVy@yA+xW)I9(zzI zsqfgG(b{d5^L91Ca5OlD#%Ot(+HEdV4aJHKNpwiH6iIq0U`6M)J{5%7SL_(4&sHCk z1m#Jj2qIFTCFL^9l?cdc!Si6*$0tw>8th{v2@d1cedxcXuFEiy?0FKYde**Hm z^K*IO#zQ<5mvgqI(d+B%hx}>fBH+5h5=a5+`W`49OH>ROnJFU*dm5WeZoE!!G$6m@ z2&b$l<1u+yUz@eJ!7hJxE=!|et>}7i!BaceTPty9ZLs=4q&sy`gWKhhQ`H8bChSn5 zoSmx}B6`|PDdc7k>J~db3V1S5FTb`Mbq7ObWko4biIXSRBrdX8nSH4^Z_D@AS$`Rp zFKbQVtk763n%KWa6%TDSRh4?DoqK3(TS(M5+9vCda zh~DRn&K7-hb19!pUr~tlk|!zdvM@D&-#$6{T=jL=pss6|-oo{q#PI9vQ5NQIt_Or- z80s**JLtT~Z7ctFFD#Jo^&vs!|2Y;*>o-{`$^+@YE|{=Z>*9ny@8Dlq$sIMn9iWua zFde!4TeI?Xm-%FV7oy_sR0qK~ug^-F(qzff<@{UZ>rFJBTH2VvlUV&mYqb2e#44m` z&gr^CIgz&yL$FN!H|zVVa;yYi#E0P0q$B~P3j{QrMZgE3e-@Y`DYL_pz!>h7@7}?_ zl?V6%SW$D9^hrsv4M&94+UgJ&s1l|7Fe6&o zT1GT^At@VzKno-svYHk?MJ}wS{7Fcj)1SlzAmvE2cdKS&k4|ZB0Pr9gu9&e9(O%e9 zy}_eE=tgnzq25>JiS?RXut3jiL(bKw`Qm*<{DPjgpK}tFriJZ|XcDfvtU;2C0;mh0 z&m$6Hx>E z>z_@WVb!I4b_B~_H8iX)xELE>xD(s#J{ZI?gppi!5O!Dm%lmg+V(MI+q6hYDLC~Js zi9KytsNo_)$|lY}u4q5j%mx=09d`r~oxIXiiiJEu~hK$C^f>I(D_H6#-)Hla2zpvo6FOuDFfZA$pUaRCue3} zHcjM%A@EY@EaRag5QtL*H6Ol(pEtc?;I5jGaMC>{d?RfCT>{KntSR^CX`A9BJV5J}dELbodD-REkIYee4XOAs8uWqXIM^bZT@rT zVuhUNcBFh_a2;MDyKuoTFbq~I_T{hm&sX5u_g|~dOD;KI-tgP(qG;R0gWRC|d87$R zr;@+SbJaXM8VK{@=GBEH1~H+Ja1#S10zrO9@L!XY%&-llqZ|5g2FZ3%mo5MHI)iua z!iD1dl9_=s?sup=!&!_s>n~Qlq*UzgYdP2npTnRqxZ%H6dtc+;0?N4aL&Qv7K~dUpkRn#HCyMtQAi78 zIdBX25mQ8W+;Xp49`Wq!=+UU3?JAVN(GubLqfdfLoL4hyntB#vnzjZ>|9)|#7_r?u7YEsKfVbp}{Nvs}pm<$Aulw?`!2Ik&j`4i&p#q)q+oiR&xC-)LG*Nnk@p!+0?4%B_ys zqR$Hsz1NS2JF?Z0 zI2~OCcT$t0+ZMI4^33-x`I1mmmD%68rw~hpUf)fOP*GN0c#-{Q-QO9)F3%u(9=kFO zU%GIx@n;fO#4ND({aAyJ;CZTd(?K1bs?wg7u?{*2s|rHX1P*}LmtFKxL}Ry6N%U zB`3xT8Y~>NohNA&(STc6I-C=xg@B^7OD%&-7RQMr0Oqt{73tE|i@7_*r<`D_u6C9m zyJz5I{D8UN8fxd1sP2742vvBSel-X4kFb+DjhwIZe0&s}jzm~HTm`hqH%SpJwHX0J z6&V)3b4So-R#aN8f6zLnz2p{?kiK%izMw2x@vD8M3HguTOLFK=2TNT!`fU`rhZPR# zOOSo8uJq5*75!w+f7C1+B6q5N#FIAqTCQ8+i#_bI(#U>nz-@QLfE5N-J zs)zA62lkjOv5-Ad)0&1G9Oj?QY)9napcOm)ojynN^3`BJcw4i}zq@Ji%YzYrCdLkf zysPeuP9l*0ff1=@xcIz!7$^SiV(h4$HP@p-n;RHCKA40X@ilLHL1R;N{UFpC%Ishf zBW77TJzX-8Bwsm9S0_7r0ayR*%**urj;k8c(Q}rfn1c}^b{lg|CyMO zK>P_?412;Q}ca-`KDK~-@cc{L5~3-zwC)b>Y0vS z&1*bbdv3|5>Ahs0{w{F7LR(LfgV~Su9=m+>5J0YyeV1HaxdjA}4!R4nYtVUH-&|rr z4D;=oFfo}|J(dV6bmbUyGk-73_vDqA3pH=GQvmGh0gho9x}c$P9ts=$Y>?~^4|9r2 zH#PZr&Fi+G6QSGjEHyQr*Jm9{t9i_`3?lppuYnAf{7^>JmgC9u2>~M`9v@9Fr$tE~ z8;&|htp))Ckm1OiD!1G|ha#iL$jIF#v6+==zTk~@#f^^fH}_6>)d-2YIE}gJa+Z4t zu3lnB6y-%G@KkOfgk@{hU|=8%3#NN4vXi$*S`!98m*qS6|Cw>R4#g+@Vk1vwU@3p@ zE|&*S(x}f8^vPkQPnh+^fLSqnsI48dDQ}ZfnS8TTIIq*X*KzcY-{*xzMJHc_yd3_ZQ)qaI zi5z6mHKdgH;};uz>ujD7QIIse&`-fqTQU93Kyff(It=yxWrJLX?Y*({mf^ao9iMu8 zTRwi|cXW)j9xMr1nb&>YKe9e^pxPjWB8=6R-EX-sO^>SF{>IK6O~h8Ey*pM#pRDn( zs2HBg+VS!^z8o&&Lz|kur92=#FrZo6v32!CAO+s48Kih?wASd(R%9`dg0U#ekh;(^fgTCttrLolt+>9_m+#W7vb*i6*0HSd|p}kFrDo%-bR#Sq!P!)y&kU-b+f;ZL9{<7 zGhd4#8=jSyF>r&h%E%~y-XEcH`1#JBr;?&Pi97OvB&`Z1kPDov61zp_W9<~7Ep72z z8~#z&b-3fp6S*eud_7nCyvH)CqfwdtflR7IcF(h(14vOruLSBZ9OU7?E8VXNXr|1q zqiN7AyL$>8p4&?Rg1V4T_wOGOs1i%EYoS%Q!^0eMfrH%Wg zsVs7mONX016|TRy)peqCRx<5`dzjDKq6?=qoKAvMWeINcGIIxP6)Fy|2@Tfjnnu$UGT$!>(haUYFAe+ANt>6 zrEs8aL047QJdmS&BSbr*1Q?&0J6bj7)t5$5-f zg?p2fYL3aIS`ghM2H^|AoB7%x5_rp9zD(cRn$+i&m!7VKSRr9}t|nJn&<6BfCL?~X zvQF3gdB&ru)(_V;JQmW~1Y27*etYH1KYcYv^soEUq5WSQb^@4USC@m1gj%F$+?QVl z$0_9SzP`+MtVtEjw|kJ5n1>s*r}3_|imFtUz0iYJ(@Bh~dOh*k9q-=`sBYLIFoCXB z{lh^A(5$6Y#yd|k;XPDl4``=0tA9uZ33End()n_|a9Ct~vzD?@ajwjHqUeD){Nu;_ zOKx14(Lb3J<5j{7b0Ux#jVL}iR}5UYnjerX@ALge)ID2NRAOM;3b9yG36}7R#6-8# zE-@oE(IE~(Z#;a806i1MqL`2nOA%2k{OF)S;d%?KSBPMJJ4PvPNU9RWv2Ju|WUWRV zO-fqDb*g=knXY@0XLLz=R#F%0gpM+qIq_c+N@Ppsg*m%UQ!jCy7p=Dr;3Ue( z?jj5xUyRPdEcfBVO>Eq`s94azs>%^h(*H2$_XE*u=?uqvJm<) zC`cxlMgx^H2;k(hH0E!-S|DVgm;z*a&i=Fcnyq2XwDZ@GiY8tiz=ahQSx}Ti2uK_X*JvR2DWXSV%IU!k*ao+S7Hh;LMaD%7)c% zuVt?ZdY}5shllIeYPS#HDlBOEnfG_Dp3~Gg_kIt@bfd0EB4 zd!6Drd&j`_h8XUJ=)@MWt*wJvHnnNJb6pu`a2JkloQl^NaTktYMdbX;&L=glixq~I zX`TY8xyWeZG58gJFeOW=+?;6CeB9W=GDH^)OX`%&=3q@u>I^iTqtB9gmg{`fJtazV1e6z9m;EA1L+z!#JXj@xru^qy`lq;s;Rz?0z zhJT>&sJQx0<$p!yA$n@;kJzK6(9r5`B(#3^Jck$wlvqZ_p2hfy37Ih2k?}y|xz-n| z!cV>b@Z$%?gl^7H^ql?lc%MA}0@t*NNLI&uu~e#pq*2&|-R0y3#XT*b!a`N$UgYIT z^t1UMS8w#0((F5VrggqpMR)$0ItP5GucSE;Fpt4%b&ck?Fs+2kG8l$s%WdL#9++*6 z_~anD(&joXO&%0P2vNfY4_E-3<*1f|L7oKeXFZnCa-c>W2@2W|2`HK+%nf)uSY$&A z>_&qy)a|Ssepo+-fi3+pYlf;j25NL?kEA-gdzx(}3U%61wbI$BannLb?SRDH1ihP` zgSxU-RvITSD!J%zbG-x97X7e?6rDY4ld!|BM2uW0{}m#q*dh&RTXBqmV9?PIKMwhW zq!4J(KLJ0&OeSuacuPO98h1X4*Dp^7bLR zdo|%YO#aKwdLqKII5;t$L(4;XkB_bu#I{t7j&-la?5;(;nT{MxudJ{{oWT*&4?4jt zEIaY$%0=iq*in(?eeC0mZI}$P zw~pR9=OB~zERou@kgJL}*u-w=^M@qY)v+b*GN)!)j%ZM*t8do%^;qJ%{f} zv<`C);fP}=2Rm&Aan7)~DH;B>(QEs7n^Uz5IWwH96xoA(BqdfS$Qf01n5@q(!jwS zkr+SFVSvZobMpqQgdgNEqos?ZtoZYt3Bwa_yx+tS>;Zv01&g@ z>Tg#l-c%Q1vES;W#9VGy}{zd5r2osCYMiruENPbfVf33Yy@1h zp1>ao-vfR)b2Q2b;>rsT-+?!mSmK#78Tb?*HqHwgE(lfr{2d^ph9|!J5F_2rNzJ)X zA20e!mlZuV8nnB=esvRw1izQ|i~n6(K>gE)qp-yY9vR^d zG#Uaj;^`@HN=fl7|71qz{2Ms7#c|%=U#R24nWpN_5HcU^x9b|H#Mo407V%sTV6Y;%h7E0Q0c!XFEl`|3L&WQjD6eBjsNM85Hh) z|9&0&Kx6awwU)UA2bTY`WN{x2j`<%k`N5JGcnn*O=rLHwP_`bSW!FtMV!)bbJ{VrS{X^~!K)S(oEk1naE+cF6UC@6ETq zGu}xC?^zOs zrQO_l^Sfu_uH4EWY{e7^y4$;e(*z&_7&x#6$URN5s>aVn8=RBlJQNnhg8ux_DNY@4 z&6BQq1$(-?KWlN>=Jx~F6y3ikGP-e?*x6YLU47i+Z{ze#hdX;T{0#D>BUn)iQLFIw zFS$KL6W?PgMK(E(?u9ivV%Q5et@S{6)?gx-juD-od{tLQQ`#~H1&C%_R zQ1fE8@6~IOh~pb`a9?t>oSD%^#I)(LJHPgt7mIj!5aw%&;19_i=G|)S?+=MY%C#d? z5#pDP=fgZEbGUP5!wZcyKmdStV7dzD`^JF!;i+!5?7(DS)%E*(KP z=(6}dEiEk&>Fq4QR#0%fRtrlzdfBKmuS7_3v4sZtay=sU`)S4-EB(>5{%q4ceomyA zVp)kbImsm;hJ@k0H`k^jWup03r{mX;Tx0fu+`0o6c57!vMVsJmIy&i?yutFlYFWI* zcDSK8r&9R#_`cLs>eu~r5Lw7S)KyS8+S-b+)lGCy*gKP5`LZ&QCn9^5P2c3c#<5ld z8%9I7F|qpkQ@@9%y$?f0rkOoeKK;~qaisc#6|3A$Yp(ZW^{LP<(9!7)xtlQoG*Hd_ zIG=Y-nLP^aGS+BiDFqMV8%Z`*@N{LqZz+{&i{$V{FUsKbWCD_obyM&B-@lgN3>dhZ zJVEMq-820&et;t!md25`7gKCSuSNNWSVH+25l#-q3qL>3#d&mJpn)9)Iav`1#7J_T-Bn{l%Slot?8oT}G{)M7Zx=N|a1_F_|)AjdvAuBTTPf>8RCy zyMVHSb^vpZ{&4UepMTqw&DCcI?s9M>*DGcJj|+gcz1@D04ZOwW%eiuABt1&QS<5#s zr;?jn>8`W=6hw490w>6b`%nBFa-0_O@je!o#OJw|TYsxFwabDdwTNJabu84mVW^mM z|DGe!Dl!M}+&NfOs@2uYR%$H6Y~dv0PF$`>1&+{+Q_F5*aJ3wlryRo8*Z0B3&%z?O zp#X~}_SmU~Z_5qj(y;7%p-z#R6(Q??l}%VT2k;Nppd8U=9wHyHnXJ@rv(z0BR3HlP zvx3$d#Q|uHnOsKKw;}g!jzQ(ihNDzCM%#QsWxK0K?y^ecv#YX4p-aS}?yo98n9=p| z0SUHQKiwQ`aw5@Re4W1N(c9hFR@>xY8P-Qb8%`oB{-=W*?*Pg%_O4Nq(U2jYGd7kW z^yoX1f4_5qRVEgg@pMr9-pf4QU(6Qxb2X;$z&ShuIGolnKmSTjOn{`>sib}DhynIp z@Z9tJE%kRdh4LrhesyJJG@Ay&Z9qiClE4Rwfg-rwzMLm{8IA>fub*yh_&n$veAr=r zt2MrbM;1hlQKsz4h~}oQPhp9=nraiu@3{KYPEz~MIK>~;ufoLi+4vM0u_z)$){xTb zH7z#tdz=N&~3A^Oezg@rJl1S1eT($TAx-lr&Z(w>0mh%ML5 z_uRD7Qty8EZBvYG-nB!-ullU={#fH$i8ZLW&nk!IR>Lo+`?m_-|N6yu@ZbU`kK=y1 z2pq+WK5$raQxl)i#f}i`TVNbjEVrRKJ0p3bJTWuNa(yYI&#HN9a!3c(&?WIH|DfW( z3(pBh=HD?%XniS^G%)D1?Cz;xLMb2+a#Y`TaW@FC_%qO3fiNI>JnT|Nm;KuCE*#u+ zA!A$$2L#}BBa`agFvJ^XXMd-L2K*E}KP=Wc^P@j+4(A8Pfz{zVDKZsG7}mBV zN?p$(fKeDO{^jOLM7+6mR-M5H0S=@*vAaW?fqvL(o7|e~REfl|h@Z;UOXKDY()O&|> z;kNJNnnX#NAxielNExMJi>%0qvdZ3MOCf|1vJ#RcDpIW#I?o$a<`xSSq-;SoBMUTc2eoO8!Rtlli=O-WftRw?Oc+Eh zvVpE%`KWeRUAb}rz12SC zG9%AzZG^-18|rWMZS~%CrJP(aMm4FAPSg+e1WJ|1_1H|k(vRV!0TOSTz64k?qBWL# zhhIy!W^^aEwJ|gc!w`c1@QEV}tLk@{#Iiw!22ojG!&GinKwU=@ey7I0gth}8ov4jf z%A2pUoKIPiTq!hEXC=zXSIHiwVjq^HNrL#X;g9!k^U@Hz_Bj(@FnlPrQ6SLX*0!r& zCw^na@biD!n%#w2fzVW$GMwMp6vN57l~LI*LvX(+)gulyrn{#S z?8Tc&b^{>;<=NJ)ll>@&R-gqLTC9FV*cX*`lm9zK5z>%;K}kgyB;~bh#{u?tIXUp3 zBykuFl-!?`TG*LI@@LI6;!g5;xeEdkDJyeTu*BE!1(mbh_N#P%l=+gE>+g@0y(ZlC#$aZwb=(s9YJ7{Q-m9L=WqGfUK6ZG-4K1wB0hj%$_jhKA=w6OLR0O{F8k7i+5Kud^4Yt%9X+5=`} zw@t0?O`ZYY1!uM8xhiRX?Y$=@HE>QRRnw5;^aygVs+x~9bHDXM1UEjEB&AvP97?IAu)E-W%K z{6nt{FM`^2=k6oxW@mr?dtcsMVIU!z5Y<=as(zD?))h9Mz$XZyVxX(Ka+pl3Fti}6 zjuBaPsJ0a=IoK@-7+~gLRB8KW=e(=tD6(N9ZFPIE@LSAWF8rYCP{d3>ja&|dd?w~t66x{ z5E?p8sValPhQcSTC9HQWxB))xGSOaC*ZHPLoe-njTvzLd&!!QwkVmOs?(eBaoyL&h zPup@z;RH#Zo1+A_iOv%244R6pJtn91`Tv>gS-~eRP7~Wxv z5CP5$&B^mjE}g`Qne5>hyys<*lqR+2GRdD0r*LjBUMj0V=KsW`Vp*HH7jGdh>=?_1e3163WV2fnMqBNyy=_;ZiJl&{qoW{dVk9f=4c-IKQB&jT37^rQKaA8Vmwzuz^61a& z+`_NuI6ekZpc(~$M zJu102qOiVB{}VlBLANT`KS4eTxtQ7)dU~VzTH$qC#}*Sc9<~ES$499B1u2u*a7YH0 zeKTRDw}2)}A@@-T-10mBt{H8PaL7&0=G554F781-^PbmG3}FJ{afh~oLO zvu;TIs744T9t^=06~=P3&}7(}SHwPHMJ9POgctYpFVJ8nwzf8?sabQ;DJvqx)`89u z7cWO`f$$s2qsNX#gqYxc#D1+Frpn8atd;Yjwu=*K;eV@{x3N0U5LhjHnogSHJ+|uR zzV%fE80Evee;DGu^$ms`ozsrO5L%LameANMbe2Qv!;MU_HO2UZK16k*OIAD?^`AsC z`2VTA%1tpeLpXm=68l&NF4a3WM@cJ+`+N|!Nhnq45-+D6;WmD`ZW*RLzUYS7VJQwK zfjrBuElusNsODE|gNJ<@V0-E_F0~mbRb!J|Yjd`(L!;P&$BbW_H96LHb8DL}x00Jo z-S5Sx(RWW;Y0*vt-BwH&!a>AG3V|eETXXB|p@}woFh1zNj!TG(dER>r&J1#8Cs#tJ znj_*C0CMxw-R}`9jVhmbtc|k;> z5Xm4)OEZK420_T!q(1-!a!yXXVs(Dl@^Y3O*Z3IA!9#0A-4(^RuOJIJ=B`NghbpFf zx6acF@FSucL+11C?fsbe!4U!HJF4KfG?ma@S@EE|`_a4Ou`PxE{&WLdv%As{y|3(V zpslnc#wuxd_gQr61oaDveQJ@%-vPVK{gv7u%tD0CFjaNh<7UTk`DJ+R8OlIb7Yjg|xylo%u| zA++NJ#vO_VwgLevMW~WBABHq2xRvph3{1OFq7orQ023)Ln{v0niE+cqzn{fS4=!M? zrn&^z;U1H;wtv@~drySiBupY2+(8h(kbX1{KTbSizl{-TnL50&6ZiXXuj9fwZ19b3 zZxBasagrdYG3W+w+81spiLnWRI(32sG&4T3FibV4PI*JwH50F*0=fl(h$FOB=M+Ts zpzJKNnfWlb`qu&&2gfDb`O;W+)1HIO_M^5h8CZgb0g zy4ByrXYoWQEExEm_}0_&<;r2N$9UY+K;HxP-~$+UL;vaVNDR;5ma=|Dwi3avslvAJBW9q=E4OC6Af|`V^)w;@iM4oZZa9d>U0pB?r=isV zPWQDTs^fi;7R*pm_4#>nud!>p)SO=%;TmlPzz@c;A-mXRetn8k{wQde8@Ne${CRB7 z5|~RIOz5A3I28{b6v^}QAoe=}zccQsbh$`Yawf*H2)Qsd-mg;*8#5J~vvW4rgRlB3 za*<$He3s`Eva|dKd!qr89q43~-N;sX}2eN3KmQXhPM~}VK zQiL9wA*hU7h7)p`(VJjx96Z#LV$N{7(z_QM9#zFNeCO>jPlsIzltn42mBpSOxxuJ( z8NUDBdXTf>B4Yvt+-o|(d)^7{xBl7cR){~R;;xS2*YEu+{=n1g(EcdYn5SOa*G<#2 zn>S)eP5VDfM7Gjm2OIScn$TVsJz9_DgKz0$8vb@X(Q?(PX?O3VMv z_0DNAzW;$@<$Tb47#r^bJO{Y;Y$BgafjsLwG@5U+j?Z!J%G0O8`~Oti@HkX*@d@W) zY{lMd<93yw?kZ>6YuhnEjT}_m9RpQvHKtQb2umV{@LI6Qc0lF?MegIluWSrT$L#!k z)qkSN`vi#(umQ+Z-fE?>1U$RyuqRK8ha$)XnmMcg zgMEw^>`xc8avRYEl#7j}JOs(_e80OFWy`&STp<#F)LbaG4F0lyDX|KD#{5}wZA{!i&$Ga9)vs=oF$T0ZiNcGbEg)csDJaq~ao1gnN4W8#Zr=Awoa$gbaDLKsbUW zgqdRdGy%xt^Ml>-rrVF4nb8{BoEs- z$;i}}XQKR9FnYQ6wiTxii^esfHp1t#v84u$9_(+fsqx0(SnPt%Z)ZV34&k~#XV}p; z4OFD4wL9pHL~93#*&7{wQm4N%ty4BMv4gS7Yw8c<`}dmAKBJUC5_rK7UT#nkew%!c z`U~6yfU%a>oWDP(K$WMO{ipp86d=w*Zo5O@)O$|F)vW^rI1)+6`8N<4)iy8yljfCI z;ceJL!<4ia2p)}1o?zGa!z;aCX2M5E*>LAD#)0}!+^~j#tE4;agFc&r zy3ju4{etLwt5q!_iiLP3{BNNA%14M|ze-`AxPQNv!GRja5Tl=-RNwaQl6~>t&FU|E zP5!?vuws9BL|~i+{Z&#jp==)Q$x}|-D1{3O4=nQJYc4!}4oVVw#V(vVY{hyu1Bd)0 zom>r*l2&1s|8-+v$KSFtuwxRgIQ2o4#)@RGy5Hj28+M8(@B%qE!GGYbt2yxDs8VV& zmbo%A!sh3RfZ?blK7;xXEchO%@P5vip*vwq7Qnqh;+^DTlh|x&R(MGX3CK&5+=`;) zdy?S>xpKK(FFHPrHE(TBCVK~gPliqs5EKp-Xn&<^86DVx+}bLhZ{67(uopky*c-0x zu=;mV^ZNCUpFgGZZ^%-ay22>b7!?gHNtPK8Il1U!<~8f_Kq<-3Xtm*Sal&Uc(3Q(i zUL^if?6olRUg#an*DcYP2caDy zjLnx*9(G!o^p&y`zS2nSkUm2r7X_UcS^e(kJr%vT3*Yb5){YDv_wDZ&-`=Vk@QPmL zBUxCzjU|nw7Gsf|^7B9>gU{|KPI$UJgvJjjEPm0nG>4N4a&m`Il;F}`|08<_DF!>a z4%3Q}(lBTb2I92`G=ILO_F>&@*@l#+aiuQCKq~mx=FkJq15{i6^Ct@!YGCC^1-eXe ztC?Phkv~>u^h3GBcTYg~_xHq7$OK_Sz-4-(^o{bppf}tSzX^~)pO+ZzT=MoLHJ%ax zbjJDhm)n2dAo6YX94lgQs4^`e5L}5u&&%<%ExSas*WClJxHR&M5yckCVFf8I4gL#~ z9UT#0}XSKUa4Fmz3)36Ts~L%i5i;zZ6g$(=;7>F zwU_ktI^QUdRFQ)sd_Wv{Z+mSK-NLV5{AY>C{602aeHs&E>60Mz6~$OpL-zt@n)s62>l_0++HVciH4YgC((_c z51knlJ?nZR_`v@C;85KQ(3h|MtlNgE@t5%nD{M~U zIut_m&eUgb2S9>5TJ7Q-Aq|>?uOo`&s5X^TsBJXWvH#=Q_M|I0kpEqm^im z2E7&HmXuf??PS& zk(3Nw$q#peDu*=^i}xy59wY#F2Is`1+yUJddP)9`3Xp%G68u;jY|6lt=DJlFepMw4 z6u5KkxtMTuqkti1;HRmf=zE=sU=WDxn&l~*IeE@|%+0djW(uC} zi}EFqdAN2#;c_B@4>{}6wY}(@7)0OmdTb3DMS5-Ut8xbc{=wRl+~|hwaim(uqt7L% z$%|LsSr+Qk@HBT5v{`;cfKKok^ha1`0RjGSx|Q-;qGk&^z_Z{a9$adCetwJbf+Rdl z9{cpkmIV@YnUZNH(3KQJV*X1PjA$6woKp=dh#v%y!@a z#fb*z%f_PR)njhT3RB0=ymRXwk8?={HF4GkCr%x{zdPB zmGGbMOlf_V?<6DN7DAB&P+3(Ckr}RTwtCF>YdwwCJ549D>Z!&OG4hUhrHd?C;PQpT z?j%vO5zrG_01tbW{cymmN<_ji4L?t~aM>HUC!cVZ=X zLQ*<18%!KnqvG6l-x5zUwQ34f&wP=euUT1M-pe8|UK65G;MYRI#sj2`d}bKqPwa@8y|n{P0u?8lHxpxfH0 z!T$Zddv^=ABpw4C{7^DmtHyA%rUJGSF}-8^Qmj&XE22@IwX$|)UH{b&H`48yfiPUS z%uKG6OxJ`Zr%mtO;2z`TJ2PbWOd-SZMz*_AzKDVC(=G#sTPw3#2lw+$IwjuVEgV`Q z!S*(G1`zL~r^mKukBwUoRlG0RzkgRjtxympoIRW8?bY|r56dI9ChbQ z_(J<6qaRUcrzkzWqV0{e(w^f(`E@D{`^5ZC9LUbp4De*KsVq5;k-0W>3FSyyyNhmg zbd0Uem#n3&;10*@*Lf5{to3FxfBsBA4qY=#@E52c3#$qLKP`a4dtGV_z6TF}I1rc~ zFn5Eurs?W4fV9!kT8LKflGzkVNWc1BliNdixHw39f37yYPEmJ`{>QAW0!orEanJr5 z7yjV&2{cs4Ne@52OxG&4v)k{*i;|KExceK^g;t$Aa#ALkifdu<6(ixT@WS5 z>s(ucxxrr6u1<-8H70zVxkBRNipIvnt**NUWRVk6`c_YyC;S4xtU}Nc|H~hzLYvI__Niu3n+TbI|C4_BpY~!zbx~Bqp8Y?)A-9G+S)?? z{rmT=+pRS?rrIn^Hsxn4u#Ya!Sx2AhrpNCO_068fjW)`kKzV(ake@%l=d~uOQ9f0J z$^I(CuNhgn0;Y>&Xb2mTH=1mB?mp*O!>IJppo3VFxqpb^kJ)0&Sja_E|BrLqv(2kp z*C06@Mb_N?>f5p@s!hi#tgW*zW>U$s3Yio{4hzsI4LSvkT;6x-!-)89SOX^9)jZnQ zKOYvl;C))~WQnT?J)2zEzqZaZ0lL{QQ&JvFvs}R{ki?vRh+_K7SFawH&Fa<(wQx0#5nk{Zd3GUl<`ly0IM`_aY%8~6ddXALtx7B7Hln2?j z<*t3-x&|9*A7upvHdK3BHBPF3OG|$v!Z$DMY|jnIWb=Rg0qt!zC8+Ga+c7pf%EXx^hcj>l% z$=-hPL_kgI9jEUhoMQ^<@1DlQQ|!g|YbfWKxplPA$o8^rvZ<&3*NKhKv`in7W^(@c zNDZwAk-|h=;=ib?W_K+j6Jg65yjTU53o5RRc02@h{(he^`@><{jtpfd5h4#K}2VL9t0W3b@D=iKS)p zK=bn3DWKlO!E>cdu3>y}dO)D$8>`yPZldJ%?|pA?mgPBeCp6oy+MOM5Q6`Ff;@rqb zMjOr>pq}}Ab^fHZU~7@JGFiVi#on)92}>i=YV@?O;t!rarE^>EUSvIb3?&5t3zfeQ zJo0?5Lmap3!6hDAs_dJ3S7iK`f@56MAEnmk8()Ewl-$IeYgDU}*z3bm!*1Gt0=PJ7}M9%mqNdlwr6tS~Ihj*;W>^N|+gNAEgr3x&&ysQSic`UnT!R#dg z+S}fS;Grd{hm$K#>N!%7Wz*DdI#17=eTb}?O@B^c|uDlX(VdHhbi&?M!g+D+=e+yC;#OwIb8*c`p}1*M&;e`O~~ zs{Rh8w!H>*KRhpOGX5p>%W#A&m+ik*ThWA?Q3n7-*RNk+xF7rU_L*j$+?wC>@i8d`xGBiIi-y5zD|gV33FQsk-iMZ(sgacXJ;%pSt&P`JTa=F zWHxK?_Te>2ku%ZRuhrBk9(E^wJc@RX+DV+)&u)}B&DmMcZ78S?cJ>$en5h^-b(x%uK&dj_Q5Y;7YI}Ge>AE2lIs+x8EX$<}NBOmxLCM0ZC<-OqxILr)lgptV8eLb3XY z70UgJ>3*IDM}NN4iO}SRjPzBO?9S6p>L4BO}vQ0Riez zI92OcD8a3pPxaU}S2H+8h^~v&LtL%?dyxa+j86O#Y!y?g*}+uqZqlt@w<33?|!d z_`W~%VM$98{z5cPT0egLYP0A>?VFf#!~%Sa^4V;#Umxoq#WplRubMcRnZYz%>^*gO z{JgBAqeN$C@<{m3YOC9H{#)+oG@PIZu}XYf#5=Vg3Ms;u|e_7S`Fni+X`;nB|JyCU3A758;pJx2sAY`VhE z_S2=H7NBR@_C9Ob^}}vF$r5v6N9UO^7?s>XfrouVv@4XFgSmfkXQbq2tco`sE@N$7o>ta1v(qQp@>0G*kxqIZt>ikA>=DG(S&G zt?jCQ*`fOnG#U@954e5t%Y{MquPS7=YrMNvSC+K)x{Gnt==y28X2`CjiT0X#*^lHd1>4KEsGzc zvqKvdC_s0WMV8K)DR&n2D2H+CaC(o&4#QS*@%0l8o% zRiV?r&2D{E_VVIdT1v;yJS{i~1@9U+f*?0&x0=3B{r4eJiMd;OA^P7UtwU456jGs8&L5s1=}?~U>{Bu!WUIQaVa zm!K~AEsBa>ww?(MXGg;+A7Mxny!%!Ji%h@Ejcl#$zlV2xTfLVx-s%*@TxD2kMBtyF>p%{Utd?(%FU5ZQ#(oWLX}hJ(yl3LHM>*U(pPyOQ&%mOHrhrtkIAP? z>)ic7!)kBGC1`K{X?|e`QS*4ORPH>HX7Q@NJmOW!nfeU){{Y1u6Hcn32h5wNUHjJ( ztOm9}CQbzedX(&J8^UGZn>kwxKvb5Nrf438V*2i~zDHdQZ1hQbkFoy;br&>Y-jTXUM#xQph><>i7pwL!#hRl}mhWT;*Z<86`SGJhxQ#FX zre!KL$De5&FFfGq`{~3eU9b^Th)nDKJ;$wnX|@JYRzD7N7xQrOd;Z)U6Z|;#o)-UP z?LkL!@#t<3_rwqSw;?F-3L8dpTtJNjg7qp^sEnEU%=jPt(=&xFi+#7Yq5%Iz_~Z`~7* zxs@Y%D!1n>lahs&IePDI|! zqKtjn6-~_>=C-+?&4eSbnyn5V9w1`f^=G+fT*7{1J+b_sx25q!Egyf^;I9+OhS?2O3NQ@&%by?og;TrD^e)TfNuN(_+>QuIpgcx z`x4G}IijrJX?d)nV}3KQq1?A7wQlmDk%$&9S4Qe*q~$)y$+1UC3?cRXXf>#eZ3mUw zIxjSwz+}v=c&|`(AR2Y<8iVNc&>g9bAB?*wSUHSYBU$N{Z`$v%<&9yXc!W-shA+DG z=v%GxW(DL`Qt&J*z0HT7c!{5ODQ2DN6p25L2eo^}?r6mz#WOp1F0@*SzJlmSxT)#G z=w!H+XrpV@#vwv(so_dh0UA#kZyheu|2j{-cgA((+lx}g9Rma5y4f*glxW@YR99Z0 zH8yUi`~K!S_2T8vxwQUmzq7;^tEeI^1*=#D=NEePw|9ySo;Qq8$V6nF;j<{@#_L!Y zcIc}NEe|y%ts`$sg+5f4k5A;KF85>&!jVkCZCt`9w!MA9t?SI$;>B54AK&Ug=HV&B zov&vkjvpr{!$2I)QU3c}jI8nR<8!Od^bAUFgZ?%bzcQ4y3+5@2UsRYT-D|YE1&67= z;DQ&@YKhbaV($x?)*`4<4lDzdCQY;|#ft&sm6X z`VIVX$NHRNt1h7V#LN0r@jg_)6O;Ds{RRpOLHC`+8xw59h#>2r443MMO#@3*L;Ek? zONPl&v<4GqcHj7BMYgsEf4!8_BFMuE zbRwL@1GPN7DrX^G_|=nzpMpH%zka)KecCtf6o6&Rpyx~+Xo&z1wY`oAxoK0HN&Vnb z^~CGgegoP}%@|K6hZ0bcOo&G=@bhOX)V+k7NcR{XnVdYr_8rPuCYQ#;B*c#~$L{@j z@kA{Qoe41Faf$dCtJ$rwv!IdX)3h@3dQLHlE$p5YxvE^Ent5<@khx!!!%>-P5tN<0 zcGo!~YwPG<2c-??hSq``G`71dv2on!*qRh;*g85&gGPf^KqaN~&9k@znB7{s*WJxc zcCwSD*rURTnSOqORi(YVdm_k_+8^1W+J(L7+<`X|@Z!}i^&^klUXNC8h~`;pHzqF8 zhG!749~?`(Mm*f_vAC(GVT^d|dO+G%y*H{RtjUrup)fBvgiO;llW0aa z^vV3y;6VQ(Pez=ip)pr>G)#DO4q6>^`yR;JoxXZ_h`k7r93@>c9&DC!QVXt^NHX#7dN-c=N*MUvdnL zXKWLwWaD;eWxsh@-upS?lo!5v{Q2pgV)y4fiWGYv%fh)6wqeeFdoRLjBz_Y2w`c0< zzLLiL?t^zl6xB0%0TPvpOKfdLRtwN8=i3^>(@5p-Si&g~9H+(quqC%@sHbLRXG4t! z+e@_O^FE^M`%lQ7B5|PU=;LpD_STIxA0mt4C|}kE zDZfTFXeTAAcg)O@n$$nuNj!*?9C%H`_}q!)*dhh)=+SQ}WRF7?*aXU43g4M)Y&ma~ zbD9_iREJ0q2_G{*wz*BGmBp0MT>tZ#t&6n5Q33>i=ukT5tr*Gq@4s|E<=o^0l8+Gk zBgZPB{#In>?7R&Xwc--t(lD=pObcmml{N(Bbj@ z`Bt_$uFkQansgVcmsZ*P*RE_s4oDz;ZRhqlHyBHLx${f@LlUs)!{7L+eqLmnr zHlo`5DLV9WYn9|tg$Na;RKLq7o5Cn=CRpbCp7ay*07b9jYjFVP;=k-kp1MXeARbDD|LVG<>(WY_wJgQmwqJuQmZpvc8(=tF9?Y8 z{zv6D=}){Sf9qP*sJv*?Poju#xOdmmQg42S%m`lEy59_w)=#!=~j0JFR1T(Gl4<1->y7axC=PzX*{8KA9UfaAfy!^;PBbr}g5P?gUfy&_)T zM^s;=o)o2bTaAN3oBvGKhxY#7YzbMfOHaI;@k%V2uYc9NJ5KujKK=vk9M{6Y%qKbK z#KwoRt#ks{{qciz_kFrOdxSAUgSt@7q$0fQAAJ+9vdf`w!Z~hg@FNSP#Ass{o%ZGhK5sxNs{X z`XotY*}`=90eozWdp{}-f3r)^snJ(~m#@7DJ@wR&PX38V-I8Y?>>|h>Axes@iaR>M zNxWiuM$51IcI|k`5MagwzWIdT`C`^}=#VC-90%v$dRi5IOsDyM5T~3-S9grLer(YV z_?DMfPGc;J1fOhp}>QK|nPQ~vQqlugs&?!nVQ1&#)Ztd+C&k2BYLwGiv#LrARsr2uBw=maizWaTlUFkf&gQEv)#qQ;i<(|b^Jwc;{hNf)~ zO~`Xh&95cd?}$!e&T(=MEpV~!^HZs~?2G#%Vb@_0SL>6G$WA#aadfa& zWEX9W@y4>*bFXDdoc+&y#c$&@F6^q`{i5gi?anufKXS51m%0K+mLObaMLW^?2E&o2 z=CknYYvrzI4S_vM}#649LTUPEHOJ_V)Pk- z*bgvD!VQOz?5dSpgrKWA(~af8Z&S3i9oM<}mX;(A1m1*xs2Yu_k6)k7AO=u@I5yWp zsxKq{i~zK-2j7RY3w}jenH;LP#0viKE(Qpt&D``>{+$D)S=i790s^!}HB|Iix`>EubtR(1$+ zuTYbf^Ab73$NSMj1iq1aH)12?BY*umT3S{|NB8Vif#ba3&~E=$;cTtuJ%=Tm5J@Dp z08VVp&q(_mSo)o#%rF`T3Zer)ugl4c&x`_-J(#^QVZX9hcf!Ge65;<}ytIW#Vt@&^-PC ztt>N#_G{uv2~mrz!7R8{SK0gCS}MFK?h4`z#;%oqn~OcyPqPEY%YNy zZ_?BiHd@w~3NcSy1*nLJ1pU*<#(>XLN)B@S!OsW6j(Z@9)FTejry1pcKf}GwyWRX$ z)smhE$=;#Ip>BA0A^fHyy$>#xz@JO8)5c-xsMz86MPg_%z$rqR{ORg@RnC^~0?LUc zdc0~e0$mo@(Z&c0Q=X)F1o}K3+88pQ3x-s&JksWw3NDp51<*QRjs?&FSiZ?I{Z)t9 ze&RdupE_gtMva>qCP!x^6rX_3)CO86^j<*wwipm7(w2D;T z>r&DyA3$QqYF5?yYsohr6-R0$Gp>b4G>pJT^}4}ftl=*-RsbtQ3K#||6QKtnG|`oM z!WZdLEWXaVFm}o3oDdCdk&(Yk|2B078Wwktq;pA^0N9X~T5W9H#Oi*o-O!iQaaZ9u zQPby|JG?bl74*lVCv^@k5Kl3-vM0AXVp@EiYv}f1ZB#zF#vL~lN~QRCMzUZmyD)W| zi=RI(o!RskwtR=7lfAUQ?tad5bsss?CYME_g@JUv-jThxmxZhSPE6eU*XC0@_`23C zYJ0yL6=Fu{z_p#q>ZLH_VWRBElgAE)zp>f9qjvr@G&3p>4YELrw z(9%||icPz0Tt9E~X|1TeJvb{REp1P-Z6r1w)l2dm*^^WT7uSw4g{5|JY3BjrlmmtR5G4Bm%!(bRVZCuCm@hu#5E@JD4_bl z(E|<6V|LHb@uj7tIH@c;oJ;)OH|Kp;YV}jW`}e)2L#9EpTQ55_NIvszj@f!g<$BGW z)sk!n(+D&8Xklv z(Gq5k+S+#vV?=w+^`N|w!#@&;Ow;<3vty2cNsOE15eKm&pyg%LSRr)R79%{yf)cqlUSF*rYR3wKB$C~)Iko@Lh^uC= z-@b1sw#0YvwuzNY6aTueUmiNK;5!cn%YH;=Vlpmf-_P_LRh402KnX+PNYtIzIYkX( z9=I$V&(AgEiCLH_wzeEPKKJunoqZ_<)Ky6HsUMXld_LLM-sVlwss=VGmHjQNACk#v zU%pE&R=1nc*3zB)^JlbdU`w=WErB^bb*MckN}s>3Ev?mV{ogSN@ml%U5>uaz_m(K| z#_ZqzcWs9jH|Jb##WIy{_I?V0*dD7LQd<`uJYcG=+Y&jWaOek;OM!XazlcB#2hD5B z`!ECZa5sIkxc=_OSt&i+Z4$ftO8&NjJToLk#>>4s>=-|XSK zt|+`EBPPC%HKtV_6ywBp<Q!yg3o-{T90@!ec-3{Yg9)5hv1|OT z(@<6g)wJU^gSe`*lZ^n!H=oTl!xV2}p?I2}O)$$Y58g(va&jUvGu2AV%JO;`Vs8e> ze}Q3)F!Fgfk3f8GBszsBT3lX9X)So+J8&RxpMyy%XKeM|#WcCuoSj_&-+|JBBIHp` zJAm%`@kP$);BHSYg@0BV0$pp_T-%||wF3}D&Aq!_ zttCPat)W&6Q`4xl_e@vOV*kqQGvWPr^Cuv}0nv)d4mv{NJ5Aqa$UC~mfse!Jwy%6# zi}U4XwUw3$w%mU{J6xz6)bJ9b^9L#!W}R<>d2|hVFa*tyC8VJ}M8}!bk@KeQ^#p_r z(O=uYYu0}IcJS1xew~#x1Yuv{j!r4>g}ZRde?$X}%uza#4=ad10lJs&w{lt6JNgaI z+;P&(x=u;XfnOH`HXHyNjPJXYiHIi3o8WhU$GKWD=asO&e*U$l_65H;s&vPWxkYVl ztlW%UTG7HGLNBZ+%0oa?0f3+nUvN{X7o`&c3D?oC&R@lJsT6@jXv5nk;b!&}26X6t z3sgQnT2&0iOm)^LcEkuZHj3JCvcpt@k=tU=#*e0ziu@t&Tk590QX0 zT!$ky4)nR>O-4PmrLwXw)qcO>o+`H=%FHD4_IVr-AOuOFz@kLeBFEyp4Jy)op`q6Q zmY1Kja+q6N-~6{$T3UOIegVCCccCRUAK&rm-jd`V+o}f}b5-HLe#si;|9eZpPThyH z4oF-#d*mW|j&}w0q#UN2bO-6Lvh&e|0KkE61$lg@MMfaBdFST12mchzC-}dj6aqiJ z0NRz-_S$T5Wh_Ud;gCR(>pqThgIp{S8;L5-u0$Rkwvv{64wpYXnUSqtR+GYf3C(!M zIXP9O=sBsh6L`jQ zS>13zXimJM|DvNK2X3nJY{~d6kGp<`Fcw!*EBn2#cdKb%wzlS@rcQ^E+)k`Kt6$sQ zlk|r`b+s-IgN-+Ub1*t5x++ty4^1i4f(GS0Ys)GWgB@Q z^n`rmJX2=hLUzhH0p56)c<@Q{7&2WTR2pf^h4e%6d1}#*mLJ*50R7^NiWK1&SH9V3 zC0aM~(#J*rOi0i1SDEH#E+oTn1uY%5?69Y+tR$0>)ih^BJqdc;3{~zz)KqTnm-9`z z-b-$;UirZwuXM!n&FR`qP3{=p(>kP~vi_ZKlF!R454r@79_07G{6sJATJr2@RPv@q z`y}7pLLOI7u4psq{6tC)?~i{v8A$wJ?zx?={=0R1+6K1LH~tb8L0#Ez07q^=eg11# zs?!Ab9OK)}sn7qB*V3 zt|j^>@#qfPsrT>TI)afn9rqqr`dv4pOBt)8wssFbAd1jeVBCl1sVXW60_)3}nt3)i zXA-+&)wxDt2*M>opI%<|Ml&+22$GbR77Kt;004~gv5S0O$$I8h*1CtYxG=3i<>!t5 z6-(`^_82V6ap`z+J+eWCg9;lxu&hgTC3Uo7!?!1z@6d#t*E?-;%S_hc!}?ZZi0{6L zP4xD_edNN)kpn{oP8N?3hQu_C*$iI9%i6_KQv;7lt~E`wCa>iTS0=pI8{gTEgw8ZLNrG|ktqO>_r! zb?!NqRo|0TgE(!`!72Cb{s(5FV<#_JTOTkhu+ifDe!K9GX-eMPtK6aQbPp|9lg>;+ z{c?%ItS7txWeH$I{*7Q9L-b1&Pw1z&wmAVf%S&@}l)}}2=q`V(@vU0{pkx6UJiDrk zdE`}g_CmbMyZ>D}B`RxPLVbOA5b}74H0YRD+PcOg^(eKo-$2O!&*$x~Ia(Noi}_Rr z9`^pbEn{n39E)-Dku=M*W*&x7>KaJ{+r5RlCDR|emyG86&rf$dQp$xfLOv3HiiFf< zuK!c@;i~YMnEfU8@SFjBo`7cNKcTcI9N=IVzZ9%bx^LX@G#&UQ%^Yc;0a1 zSL~6RW)ztI{Wc?Fd&_;)P%x!sz5$6`U%;#%h(8(^&8HBt{C7B5W(`BYmmGqbP z)1!>qL^Ctj@i;?#kQ{L&^DjW&2J*zIpcC?@C$34_$LHh-&VDeSVRUu+lus4PE=b23 zYY212MQ;LsryDQ0)eu54Etc zql%U^i~4-sa5(eGtAv;^4pr{!tVhKFFVlE>nFy(yr7Nbxfu%kzm(<_4=Qul~N@`b@ zxnncnf44}QgqKX^&NCu<*QicK8FF zJ7^QE*R;9<0z})`?u=;7`2#NGscWeKP($c4Uh<`M;Y8YZcRr>FS@zdCZ(FLe3>M#lo{+kq>Et|_i zAZ}IQ#%#}r-wQa@s2`I~%WvpQ*<2O(Cn!7o6=yU9_J2x4V^bG;aPUrRSR)%71ee$&F=?9x(|}60 z_XL$<{m|S$r+CN>Ft-YcO~x{x1xTIX>`?T3;&osYN1*8?f#|PKt}06P{Nl#66G~9A z7vN31=j*DJjg5JV2e$bS?mu}36}_xb$3=Z_xe zbac*pyq?edzOVZl&h)iB7714}zubzU_U^V8$jFcb2i06pae$#op6gZN+@3JBr`!x8HlF~3^hdAy7q&Fw9C#pA{n)e_e zCSK|kyMhU;>%+jJfgyhRqOOGHIF2_~L2#8uNLhI8*iH@tnn>&G_p}l#q{pzkUD;NP z^3B_el^H~2*d^S!(U<7RoUYM=<_x16NWdTF!yK1y)h3FRlTXGV67AZ%kY3tU5X>$! z`t{`{IKM`*MU8>oA6xV8Ybe6|>*qfXPzzEq%#yW$#<^NK6iL@sN5 zoI29X`Ho&~FWd+2Rwrd8ixR&h^tH_=FNv}WCFK8d`6tp+aFvY!N#I{9Glr+46+@9i ze%MXkI_zn$YU>m1_7bt~`Qe=6HTl->$Aeub!mm zmXK(FUFqc8xGT9+^OB#>Nv@L(%<-V6Dk-aA7~%9xxx1mmC(1USkR&S5ygJ)jwC9f! z$>&ofeDHNjh7?l;yoTa-6L~^^44Vjos^iR99S68bN zgkc-2ERgyqGQ}v$9wq|$snkGCwA$&6%^55Y+y+bS`}*rg9pQ>5(LBG|#&EsV#L1_u zydbOk?nR^rbNKnmSIm~VVM|I>(3)xKJs^MjH0NmOsn**kS4<7uA773@?&`ip55j#_ z`mb|4qUk0!{xBi{#}tOlx#DFWfEgh)YL<3JCBWbXEw4O!hk2|gKX>0X?9rPdnGpHSD?T7&HTB_D8PU3T zd`?cq+yEAaf7a9VI4VU#K0C*nK2J?Of<3{vXJK=5=RpVfXx)9I^tYAOc}oX}N83Ks zsP&6(ejggWQ|} zI*OewiNXIsxMNy&_LFcjF!CNZ9M8yU{*)zgM`&j~r2tVyJF&ODNB1}%=HR-mSVJCg zR-x_1rPHB0x$%6A9#$3mK7Fcenk-Pe{)pm}>7&8qo838rp>$KDE4>F|KRbM#78r19 zp&@Luu<$~91y}hl?qwQ4Dqw)M?5(U|M3y59eeB{J_%rewL zCqlFdC@Rvq(Mm_TcCxz^Ya#b%*nFSwq@i_vXQx>=Iiq$MLF?}=w2Vq$N3<$2RbYhx z^Cl*3?O@_5JQV3zru#X6tZjkU3m3SC+=wk8NF%zws5gIm7JKP5SJ;xPi;=#4I?{l1 znuEGd;(A21)=J`ZtHr6ickM4F-dQBLd7U4y&)%(rajDmM`dfLy(P$+_?AS4Pi{jAl z-*54)Fx{hR5}h1@7Ks&F1o@uwfujAT|2|EW*$sZb*T57#nPS~zT24#AQBK!FgWWKRec(J7K8bFOz0wA))}$rZ6og9vyZ z&Sw;+Rci63GY3D+93tBH-TX)rJK14s8p)bOs1`nh2if?xWxir^_x5^9tPl#s^7_5*?lRe96L<@$>e3{0LMKX!ncg@O zM0+8|PEsl=`*uw|A2#b~F-wp>^SmoNyPzCs#G(D8c?R05qMRXPlzZzk4h9?t%?24t zkb(-bqsH?ICL}}E!t@{U4&}x!yw<#;OydBA)bHP)M5C9h6V1oNB4Mov1a6bzQPgn(Ke=8 zZU2G6qOM=oPvd2|cd}yYHimkfOOQEE0 zY*%O9y>$QBz-4D=5#Kep_K23iMR!xia7dyEPrOcUG9)pHFMK=a3-!QnLXC|#=JtSK z1ZHvvlT=$XRaRbJRSKm)^&-t|L1U)=#7U8+c!4U9*-|O=bx$M6nLVRn z1a;BD+BL5f{lx`W$OE#oow0b_fL0Cc5T0El4&)&dzaJKN=dwGB$`p^Eciwjif!&6N z`yeL=vYVbxk81p2mzZmb!|+|&3E|5L38!ZIih~H%9mHNV>ssoN55?E>k6W80EL>uqc8UacEj^nhu18gy$G1=fqe z)kmwmYi4M$=x5Oxv^{cKvZg7)xJk34#vHgf1+aMrt!wxgSZDwlZ}Rdr400SKYPw0~ zFD<1Vt=*bNrl8fem_MT{!*#B+cM|%&v#~7z ztR>rJeM&K}(S#gs{YnHp#Jj) z!v-s`F8SRPf%niKi?0*x5IP(O*W%VDRp+0*|9tI>7n1_%j5fJdQ^eW62u6|6QzACE zBSM4{gdKmbjkVhUyz|y))-B0au-o-*s8`61mLQJ$-$^QrDx}N`_kNvvHnj1%dZQt! zg8ASB#`B#{aqS|)%#N8uVrwM6?RRV1m;So_U6S0vCLH-_gqX*kJJfnkLq>1E{{7c= ze#f1u#6SS41O-B{ReozGlP;HT0aBrXK~A)HdgM7v`1!&SZOeSP99}Ab`r*AC-pC_b z`MHHVj668Mtz9$MycgqmmO3eA!&8jq8M51S%^vr_J7eOC)jl*<8?i?{g4tVQy*m1o z2lVzKw6tHQ_@9Jw@m&~^P1lH)JHlQ2uJ*?+BVWV0f%8?CbtBAh4u&NLuV9%pTXcF^ zkKE~}=-33|8aFn|;2R|q@agYo%ONDgAX@}Z;jj9d&c^yWAPOQd@FX_<)B!y*S;b4( zuwLfvg9V&$5*2J!V4`Rfv@!1V@+uMBf}@o{0Hh7-nxb3Z^xyZ(UwykYs$7{Zx^6lI zDXa+8&S(#mvWkp04kCJRZ*iX3_W)_smcGyDVA=AbJ%uxK!E$^Qhj(96cRIJ^5gtwH zk*CY0eXrejJ42hKqwvCT{2KIkRg+ZIOrP8K?{?o>iDS35IXdsrPQn;Y0q<4KlLuzz zVvKW|ZYE2=Z(UxlH7?%88pL@2-Cp1~>9zs4_*=hx0j+q+#y&;0wov`cw{M^R{yoET z?yi#SNDODIcyuK1!T;j|Y%TNj+3wb$C|D=bG)zdS3pf{**BUH8$QMU2atcjN6@dik zUXPB@+E8vrL4nWU&m_MCUP$XhAkf4CN#XZMEcj70_c|lvAW{wXIQ+OYuhNuY6t?P0 z-PPqIoyrVn18bXcVz~;V`(ASu0pa0ik+G{*Y}LgjD(cx}(RGBRCWzU5y?tk0ZDC!L zBA5}Q&d*?EIxr3nCf^~x(1UDYJVSdX+Y4U5VJ$yUURsfzU3dlt(90x6d*;k`t8?uf zWIBkDBt&?eAy>R`aTYle4ile=w6x&rGWfMeXD~{9AfASHY3aI-P8vV|x7%-szf1-@ z{{EWSxj5LXtL9cd$yuS`>6jhJY=y+&Cm~JL4oe=K%F4uFdxc(Aw*UI2FKGK~J-fE& zFeTg7Hytk}7Xm>Q&i5h1Kfe4E3=}ji^OM}&YGmuk!~DhSyfkyf)lZ#1;c)zStDZJd zO%iQ}29f)UYACOu;74o>)wMGwe7D=xA{8hcs_fu*X5#ILNyuIQe#y+k-0VV=QsU99 z`z8hE=4X{s)!(vNrolbBW+=49{Cw$T=CcH1VCq<_20e~ZGRKzz0R!S^UzUtn6hk*6 zke_e=@89nQ_dF$?aMtGh8hByCG)mC+WrKa}pj};E@R-v+{6Mt-L|<_*kb_K>5E~QA zRCUB!DTpUSQjajLUo&~BSy@yeaw?}uCt?1^sOY3>EK~O{q|9GBJn#^MTzU+GD8&@7 zvQffvIxt3IoDx!5l9*3w?L`dE&;_OTA586Qih~8_a%(^KHKH z)yo=bdQs}xTx|uFi_k!@Mb|dApObu~XMHawrFL^ISpG3z+*{2k#+@;RjGpZmT_T-U z>t*H|1v8r*lJ*j&FZ7~hh5He;Q3W&N0fmRn;`)Q1FRD*O zD}^vq#>I(*^xqv1@8OMLzQDhHqROrIRBm^L+_xIN#;e(TudtjdF3tku?Hwh95a}QC zuz<()V@v(7jAxfux)J%d7dEl2y2hKDnMRdl2btA#_r=HOAO-`jd=psg$8~g=Yo7Or zs>rDKZ)KSbdgT_XoxS(_cOy_TAqIs#->Z`wYRtS4lAb)gFFfBcVQNZWa&*z+bou23 zio(2%`ubnLcpscT9Yk*L#x(EVaPgzrb#U+G7|F(Kch)4w_At0h`V(DdJP#Wb%QlW5 zE1zIwhmpX`m@z>v{S!QL6>bJz)?_AM6n4=k4UsdaE&@}y^52JjFAzKQL0ZqMW}gRh z)vLD9emj_~Ay#i;V>B=CvPQ199>+gg&+8gBCw8T(AC~$N&qUS36xqZlEUf!aHKJB% zZFagcjdnXrN`kvwvUc@51s*xP4y=0{LOHz`f_D;T``3`}zR^0lv^dVavj=Ib4Dvg2 z&W-;aD84rCRqGOI|HG6+L>yFG@!s!ZO(oQnTey8Bo*U#(AjM*B{ZD{zm^$l2)*#^| zYPr;W9X=SHsYbkiJ|1}P5rj#OYl8_oB8;0pBC*Z0+kGc?I3dg_?1OQ}d2^!`Yh+Lk zc-jaN6JI~e;9$XWuj3^WgIGt~%w0rPnxuY#>>O((?4)e%@wv%b0HPAV7IXd5r;8h- zSe_@U1{b~No@_AZT^DGpxXRYB=}oE;h1KbpldS8kwH8%Ao}|eXN%=z7j()>(w#fNK zqrtQ`dm%`wXywCHo!}n3HozpbKP;3BdL(9`apAaTb8=e=>gjsWPbhSP&^hyuJ9nf3W#mzh{{9)I3aQ($iaR@)AO! z?%utFf&<5_-4MRX9GktAAG7b-xfF3f)}Xgrv$PQSkOj$-73()_Zw+Gl8o!{;cK;SX z7Jq7F%)5QIdfgBk3-ZhEHuo2GlynA|N?Hq#ZLl}14A1g2uJhWg+PzA>d zP~Yp@lJdQ0ZumBR`4VY6=j5dL`cQgzPHr!CWElP37*4~vOA*)l%cunHqvYFLEiNMROpoKPv1;Ye zkb#_hEO^Qz3*W$QiVX78OxJMSn59M7TnOHKW+oB7S2=lVkCpB$_Yr?MAsjtz#`Ej% z?C|#KjkWnvtB6tX5%b0_%&bq87uVBiwYVa9J?-6n!qti;uzEeZd~whquu^ju=B`W{64cH>|_zLv`v<~lF!MMtDY+DPy3#Gj_rTP(2Ohg*c;sD zRaL9h5)A63dlTDAZjLb}q~=e;(K7UOAPo&_^2;tv9BO=BfH!tyeQxA3l4_0POW z=0L)A!NEcte=?6eZzw4IaISKbGyM5 z;|g@cE!sPFqPbsLb9*mr#P#w&b#C0dEKNUxP}im(hDl9X#w!~}uaMb>EJCLiPUBnr zMPDYD>hYkF74;E9reLyKZC&2?`N}<%1Q*exM~@;1ID25ihFitP>Q3p&0-;aUYb1ZF=c_->?`)sq;PeO2f^5c>4;A9ew>QKgpIXA1hTl!u zf(XU!xwWrBi;c2=`r`dQqaO$>RRfRW+-b2H)bSf|KMJLb`_UL47(vS`FXQL;>65jE zr62YYKcJ~%#Mt6nmG`Z%sOa7>18|c1iB^>(M~q0@5TuKf=`LP8;iHQ@yLt;gyg%gA zlaK{BC(x)kLVZ=W&b3V1R6cPgN3h2<+8MpB}lecimVU} zBTNZLNZKy&7CmXuLRjxd+nx(z?50K)D?MBp`D?A%5dbWg$|5d)OE)spC+q2H4L>Q6 z^PX@D*RIfai>%vV>u~43W{lEzkmpCPu>e>s+2{!J4Rz-nb28T+vL91 z)8W1uB6kXT4=$o%U^F{kkY)@=hCBHYmS4XY^Ue#MKd@m_>hyZ2Tlfaoz*Y$5B`H!6 zW0S7&sA4Spn(YB)=f0a{f2OhmJQ~C>q+M-A++m@wFLg)=Nm|F%rt6W{+LJ@sB?#^q zdK8XToYV9+I5GbIqyhq*FS@#(wc0eiC5tp2-lKYXaqP6c~(+jOn>(~K9x#XjL zQi1DS*jTc-NU+k`s70REu_Y(HyBaW=t>{XI}&4B|VhbRu*1M&Kf=1=*>2flC$_a~Hqnk>_V44r*yBzN%f(SF>dT zhf3Uu5K2U=diePj!-X6vN>zD#vMc%mb#f>EE?B{L`CJNj=in@ihGBrol+;#iqbmkx zFeI6L?MJjsO+%O!=|uVEw6p}QOM}bGS~r^uN)~2s`zGKsOI5#|tF5IwIiZ#Ek9l@B zmf?8Mbm`k!J6b@AA-MF~)qHsUdTB9-c65)Az!1hK*;}&#)OXVb_S(hW*4AT%dLSh@ z{v_qD-Q=IYJJ0Z?R~PO-oVoP<7Lg6fEguF;5uD&Lm7&3MuKLZOAPx#lL;~o)OR1^N z#`lZSu|S#Z3)1Vhop^HhgFKVlKa#zM8j9PupJpX{235`bh%GIh!TG~h>iFvH^fhmz zw!PJ(p*7{=KS3HoWcNp#0+sZRTaDvE*dOCprTcPjbkQO5UHZ9Ma&r8p|obe}S#OaSs*%5j`bVL^Me;K&^rS)*>)XWXUpQa$y9+Wn%X{EEIN+@U77pNuwSA;JsZ(+)C52PD;^ zsO6ELi_sU?Fudfo=tOw$V<X$wot)8%;J4sUH;7}&5%Wps4fX(P~>-rIpTf;sx(yAL@>YK!iK^g*Un5HQhJanaEy@ZlczXKZO913M_!URT%B64xRi%Lu>^i|a(R zLV}RytxpJrv6`tPJW6tV!3u~IB&A(hlTes8Tf#NK6;}v@+d$!tfotxAZtEY$^E9}N zTmf?;fvx39nAdqK+10s=?IEwR5;Ok`VqtahQC{B~*{2`1dz9 zF$r$YoeFY{k>YO~)5Er3MD1jK7%ws_rSTZvaLpG}npyEYPvtN0bGUxMXM?Z@PZ`G$ zFfj-s5L`YF2M1qI_VIj{!*(1v|H`BE6-mzcgrp~-ufQ3DZBMhv%NYtm8ia0CK8s)E z6Rkv6-Ra0C@PbJ90^QW*I8;dwR|(KuJ~dS1CQD1KZINX_%$m|!b=`8Gy2WvxxSPFp z`Uj1SjsRG@;VP)$>@1+7(l$*qqMfV#wYNVmDVlb^K!?IuKhWv-S32~xr3Uq&4B*Vz zE9HHNCY7yFH@CI*A0L0Y=oQvLIMbg#{iYSho^@)xp`U(x%&I#0$N z-0S19xSLy+bWAo3Y*!1G#+tjvD6Q!4^o)+Acjq8A?q8gXLQjv*1xW@o4-u%-ba=NW zCS`)UcHQjAL}oyb&bijsSK6U_6eIq4jxY4ygJPNDs*tEp&(_o35T%T+|1Mhf2fiFE} zI_hu3uj(fB;cCUijx>2kpvk*Q=$^_SZcjEIi?eKI{6Q5;p9D|Jk+lYoU#WKBa!A&P zGaW~eYOR#r>~C2YE?p?iWA>gF2)z8r-=7EbCSy49&%CA=>4hCqH%9j-dl!X|j8M?D zCLAYPTo)E|6C8DIA^zpmWnIWhe)ufT7J5A@Drp7s=4m4Ro35gq5Q8pzhA@&h&Z)r< zmfl6QRXd5hW@l>@O$ClKF-d?`^flJoDR@SeG3;Jt+g;xONG@ITB;qM{$6eh?g_1 zJ>Skg;-nAKf*YR@H9Wx_-Lv0TtO}BJTQG8)x&gpJr?yW2>ZW znI+$}yQJ^t843DY(E~r4m7>s2o>N*)0?}u%|2~Y;eI!19e0V#g<@M(qVHewCILnO} z7P_sEK0h1Tq;697BT#~m*rxTBJj*L{|_Tu_xibOfLorW-Wc?2^HVr+>^ItoIzigWQ&{En%Hu5SHLvu7D+^}TC5ZIrW|6D6vjkDb7y z3=wcw{Cs|b3S0CIvkxtpoh#m{haUVjw>sc(y_^7f_?Tpu?9wHa9>aS8#?d(d)})J} zHMO+#dwG=&V3}lOT5szI$ju6W0lo=Rwz+vTaAZUl_*;JdD~8C}DO7)dC08xavD-0`$m(1N7L;n5Rb(mvRiJTD-V;H}C+qXf!QQvz<|7 zmE_~wjd?^c;d(Tm15P*MS`8EE43b+Y;m-{i{CREd^^oVtR0k&mSu~nMrV*I4zW3Yo z2=gO<4W|jX*URj}#j#6Fu$e#Pc*;hloYZ`%Y15`p}5j^72qLc{= zVRBer6VfzfC*D^+R2cvdn4LcsM)x9^$--)z-D;N9zS-s`<>B2SYGAgkvu7f$da!B&`fu&~?S`pTwkWqxBz}J>)2dqmm034CJ_eoA4YWP}Gv?SRtwQTD%EhcFd4dz_9u&w68dyj_kt zf|07_sZvuzNW%@S#Atj3cx|+TUn7;qt!};U`tk$pEFNKDg2QLJ1|ZnCOG$f_7Cwc1 zxc0PR=Io)&%(sYmo|upZv zUn`!E0;mrKL;}9Y#DD*OW=b5fF0P`FqxuTrpM{4OHVRE$6cBm^|UP#?DCtF!q4V|Ttv9E)HsEF#CK7}IN#@PV+X zX_@S}f1d?Ss?pj5Jsdo?%_2>*UF=t1s*i9=lkP`?y7X|mMgu^0 zxUf)Hxs`W)|BVFi5+zb(8eCCrmioue-VtFaOYc_Juj1nJ^{GOm>bpn#4=lV%Y%edN zct0I-9Vt2F5H;gDfLr=c7X#c;WFeVWHqQCyKe18-HQ#bzi9AQ4ROD>gU&+*yrP_Ky zwDO5b!AR0P-J6MetwPod5_9#}zL-Sr(&Lh@aCfPE)u!rP9BEtj{PDf#+|tgC$ikY~kxi>H^fW zU$w8F2upcUeV3^|%`Zu;{}t==-zx^lLmnM9LG}Z-0?z$*gXkj2WEGnZMKODX7wF_t z9DrWra+C1aDgM~jX#;~GP-U`nb2Sg2<|?11rJ;kGAtok`gX3Xb0x@EME|DV34^HI$ zJP9k$T0LDKL)`1oX{SgS~ zXNjJxKlOhtErmOgP+2{1aB#^zONjKZgSjH$G$a*P7y;Z=Lw*j%58Qfk=_D|4XZ?^W zbEA0`RGL?_dNkBuJj>vMja;wXt9iLYyf^g zYRQ-98SA0-tc8s%g^a_uO68z&$ho0TAxpqbumSL2a4hJY*sNKb9l3v?jMXfp+Zrl8 zb9`hgE85Ms7V3dB%E_@<-MUr%(@V_prz{|lUSUVG7^n^K8l6NcERzPkH} z9KoDG*~PjN`J5V2FZ1&a#f76srxRNF1Seo8tq9;XP_7Gi-1mNCm=eB!cj;xSZ>I22Jd@3c6sh|;OTk-cMzAN3Ex zj8Xd<|NJ2W$UZUgJLS9Y@8f|Sc+ijxGcq3A9{Z_$Y`;9zP+F14(>1u*V2w$SR@vqw z^V8rl@#lj!Iy%*fT z_ia`V7j$qi8Gau42u27z9T*|<3|>MYnwhzoQCN2bjQ}Qyq@?7Qn#qg|o`Sbd2vfgc zUp^aOp1>OPBqaLOyZAP)l(MzTeMRPcO=G+Wt$lR*tIjI=;O*io1a~?yk)J zk7gIH@&Cqt5Ir;KT{K}?LAHYJD3$=kOxNKB+s%eDli_s;)1!9{RDip2^_zK$UgCL1 zC^oO)ko>+`i2|YZPijp_q)1V`VGX5k5cITZ49^bWnvC?(=Kdc6lHN}+5gI%8(@AVH zR=Evh%?`H+C_^A@)FxbT#l?>hAyc#7ntt#>VqZ^}7SdVsOG<>v_Vz`OIq73L06H89 zF8(qha4$?Ud*}KY!jE z-Qk0`jTcU%60UF)cPaW&^O^0=r7ttKPtow(ZbZjBkiOi5H-9dhKzGLMkkBRF2oUy$IsAjusTBA0H3BuW{&i=v7W+|)5| zM^9~e|MAxUy7grpcs zL>7?|759l#1#btb%10})-j(Bevy0fD5gikBWXhsRXk!cAZXZCuIBkFK*8gz<5|VRu zshZT1;wQfqD#pYxVoQu?dTT3WroUIwCU^R1!q9)^F*Ye^m)?JK9TP@M8X7ubIt$nd zpb6~jI}J8LK;1I&DI-ora@vPb}@QOH_)p(%on6PU>6_8W31#OvJETJ&dDM6kBw z>I^v|hzPqdoFdHdX{?0wBlwrVLfCV`8f!f+~`}fB~&|aJR%k4bYNQ4;R zSd)MBQzi5v6V0**;+{~4Oi%!YE9mSf3;_}a7!Wr*ehm#}zfBz5qJ(*4GMEo1xq1*= zcuNdn0Kl-}LUYJWUi37#yLtA#Yt;;YtHt;Y+|mHKVM+$LzbGOwtK;P@dsmYMaRi&2 zIXJxfs>2!U^Fp&v=rXhUJ2__HgqZqbjVecBR5WYpTJaM0@08qqH0t~PXPJ_MLp zKzID^-Ic4lhOZ5d^79LWGLJ`gcwh8-A5NqLoWg+XX44l)J6WWnE?3+V;a#oxOkg^GSVSI$Heq z0uOP6q2W~VfP5O@jEKvvCopR6vKk2$6J$KdN-gl{(S&kqm*C|K4ak&AStty<`)GT6 zHw{hvx567jVy?E6rrYDh&#s&TtPd%=mXb&#fKBgTG6#Om-!^g^*2~|IP7SpP=-u-t z_x2MV@>xztdDUsW$$LdD1*G1t_zg>5!f~%1(e+&^2dh|%LkW+*taI^kq=&FJf0#r& z^opz9P{#rh6JXdOFZOwMQ`6@s91jii_h+2%A1wq7*!8*9AD>yqC_C0_K?*UdlZOJd ze!0YJPcG4hY;KXtQ^;l<%&F*ufENFawC)_;5YF0@&D2WGv+oxE?mNSi#6^5RmZ47| zVLWs3G9%VpxPAiLsnOaOT4^zXSdrJP`}XA6Qhn#kg{xMZ;`VK!R*7pBUmy_v<$8Pa7a^9~!vVO1VFXj8Gk{_=94#Hz(#d;hVDbjOuAH~I;#{B{y z_Q?KuOmlI+3NA>XqEPdt2c_E# z*Kk2{#2$AA(bgy_KT*M)Dj0!@VNC~$b?n^@(J6#Ic4h7tylwxYpdk4b*Rk1IE;L!V zC6w$n*+~N6dV?t&@dofARA)fu`-T853g-Sl;^+o-{af7IOpAYSVQ2ui6V!;$S72$Zj9QuL-K!Mw5ys+o2o!S1#jy(} zUbB=K3-JZ>?b&k?&x?*lgyuDP9|5Tg5>n;T59RWTeA&+*ezRngI!89rxHy04QndBNqQR%33~k zlDOu)l(Bva!~4h(hJt`TtR5WyTssCBVsznK|yq0KZnDwf7h z?>N<@AV}%j7c+v@Qh8Uup_*~hsp{>;g5LSCXsUdYD=bO8LFW~VyfOxk^aD1NFlDAv zzEOBWBYSK{JgZBZB@)Fh>oGh?6IEVzSs`B#h_QO^_`>gCnD0IMAN|a0{*=4cnYjpN z$L-C-qDlWM_q}tX|FK+>c;H!kOTRmq7U~MFztHib(Q>VVkHBnZZ7n|ObMOJdt{Utw zkSQbN>DLe8!FhyUqo?&<6?w|WK+8unD)Hoy0q1u6&l7YHNI%~CtZrzNRqIO)5;eo< z#rcZZ`ucs~TGyJJ{bUaOC`?Q&Vf0FlpGH7N#+k4jlC=y!eGnm5(QBuIGZdM}$R^rU zJq%;`YUJc%^z{QfJAYoiY~?4F>n-Q$HI3v{pbMz-(&^z1W-UZ@o8FJn47^6ZGHy3RXw3%$;RvCmI@Rno0bCbY`Q$Z-0u$EJL;F4fXv3XZO;^8>xgw?$s^ zpV!N7rZP(#?;DTUssSe61} z)t!S&olbIF9Nkb|?ADcEE(Tz-Noi@F10NibMu@t{{ehi2Hf-;|Mjd-9pT&I#0(oi0 z7s@ydL?DNrR{SzFHvz-Hj&FLcJ^6;PsP}pnFWO>La4~FHvQ_5TU>!acvWW@uk7g%f z@)f~6IT1Z*Meq!sgG+`7cDIFk|0s33LBzJTUvH8X*w_kebu{BI*WaGDb4}F*W9*XRS_R<*Z7` zdVxD>pm&X4kR|DP3hBpYyR*7HWK5|WH=JDs-z|)SJ=;chq&fT~cr?(cU!PPJnHaIm zrpWsbNgjH6@ZSFjb)UIqB(4E{6&RP#>iRM;bM>K$6e-L?Q4BteP&B;|C$ZtV1r!)O zniKRUA7{%njjL;3YH|dD9|A(yeUR`+QGm{>sYSp6QVAq3Y&>DQdd|+y9-v0$jXMQ3 zo{zzajokd(ptb-5_owgQce%LTW)l#b=$JMyPa&*43gd@{JkJ`=DxrQUWI1EfrgVHa z8B7*t8GXs^slOjMEFe8+{P7IOQxCzmyAl1dCk=eqYU$dkMJxRPu69|&?Amihk0HRH z0mmSfcjz_V(jAD=^<$0M_ioy4*Mk8k1J=i2bES3gqu3xsvmP$6ts$r14Hzkq`5_4$ z?xZnP=bB!459hG$!CFQuYhrz9&7=X5qFUGA1GR_4y5+7nTdU9)_GRbW!B@ub_ivq? zT-sY!*`uiet~`Q*pCBHz+8$Hee&RKMml{a(o?kAGFgXA=%7tp36mQuO+QT0D+J>KC z(twr)I6aunoSbdi5A?L76YW^5S{ZSiu>U698X)N5Re&oTz%V4E9DE$G6EOb-PR!ir zDK6K?mA^TG+~%t^rI7%fwLa}{QTZIp&ZeHtQBr=}+{fo(T|f%l9eYolGQPev;q|Q~ zn>t@dMmUjh>Y911buPAzhNZBDH1y+us%sSs`gY#>nyQ(7aQ|_jYkybE!_EYebH(~2 zmi6L^5GUe!BtL$*_Qh#&j00;vSgBymv{&lQnY5tO2S2w5JeK2w8z(w|dTRlP#jztD zx8ifynAQf%{XsFVtg^w}mW*?Q9s^BZMzNNCg-)(EorL!@yZ$mWp1i?VGBSR`bgtW* z?F<$eWn5=&X~}Ujg(LC^fiU$I&Q31(hf~yp{P#+|L*N7c2PjDx1iye=$!%&uiTx3h5oM?{1jXtmN;=`0n615U;--Ah8pq zwO0zx-`ui(QCLV5saURbaCu|&$rBEc0Ew62 zOl)}BMv<8puNsz)1f`M!i!)Z>;0O}Rd~$N7Q1??K!thPWrT+in>dAYiW8Uws@tDwD zEH%~9V-*T}R0mEkZsD4m{g{70jxK%xsk@6%c8Y-PkG%*>*}VBE)M+iD?i?un!FV^Ev zY9}nHQ4(VBjOC&G*`@fPOfXi{0?YhBxpDuQ`uBeTyuT*pGB1_7Q+g=3Tm7(|8vN@^ zG6nSWb+8S;lzKYm6Fck*P7>VQF@gdmvXS#EIH34oX;K#?ogkZ8YxqB5#fG@D(SeGy zX&o+BdHP)LK=$m>fqodx#^N!smzIf2neXDMch&+wob=JDL5vJ0{QGMWfFDqz0D>a? zqz-2hJ?v!8$0exgP5|rPwfVhZmYG!HGz0}-pz?v%H?BvA8ojXTA1JOZ^*D-Jz;_h1 zA87pX<2nn=;+w(ljF{fDt20)nLTYX+GY zld*B~onTU=58x**wXr6|FCwb?;zN+!V7asXmlP-lAil3r4EMkZ{{8!=kSQD;|BS@0 z&sa(#^L0}!RsHeM5IujqjdL}^mMttE08)l>K!)NhE;u>~(<30@UR<-S$@^l+<%5Au zk7GY(!m0WB33TnEVy-j#cMLTs*1z{|2M(_%4WqTW!8=8pl^un?zFg#O4PSu&4m>!N-@bR#H{M z^y5?sYkmdL9@J#04$%JI_B<)GK2Xq|6YaPtovPQ|@b$-16VvkhYY#4+PJO~sUS1VF zubkWc{NO!s{sCyor6(59gk(Ba{654~>2~0#l8zR0xo;Q>(g9v(tP68<*1xtptD;l0& z2X0wgXH`|D5J)q>(j}QA6~FYBkHn|wQjE{PCY-&|2FpJiqo@gZNoDqS*V~gxeovpe z-5g@_-_h>T6v_RMe$-9|#2KBdg^xrdc9mf5=^GvC8Z^%3SKEFI_6^xyokNFXHMPxO zRQ0Fx-|6Y?U(X+|^tVj8*bv7@j8%N=$A@9yKSPLPa@)3h3vg(DzAA`E*v1qn^KeoG zd2cV7jzMR}Z@sclX!zYb9gg-vA8bfniNe;vMXaVwXmZN4NYENV+Px8FGKFr+(sLiQ+vM?b3fU| z`auSrCx9h{7xz__`#st(X1cwYOAy%t7W$jz`)`+Fd0E~@C0>o1_=UZVW`Ve;D zJqm0;v6hydsrV*)zyZq<0dXn8U%p&39oG?ce;pehZq67U#rrQF)5?YD zX14N#1Y$N1*LRg%S2d#C$D=PmL4BmZyd}N4v{c*WPSb~#rpRI#GJwf**rJ1qEqy=0Qsuxp_s)2E#ZsGC?;rzpr0OJu5viJo zmX{mbw>Fn1_4ETlpR3rI+s@@;W22INASF;-d;va&wUd&Y0mC1yOAFr!w@YqyNfz1k z8XtT>KzAif6Y(XEO9v{Q{n9g$ncv#FwRT&B#vm(`zy^WDtXZOKdOWWom#ikeqoi|x zjk6`~ojXph8C_P_QC9aKm{dATF7j_;!yXuT<-Anzj>1f;-GN@JC@xOxhpBDn>~rklo{5IwhjPx4>7T3$uL>4W0S zUwp@uP5&J^ChovsFxy>0(bAw=5luU`u)X;B?m8WTqI;Oqo`iDu>;L-jHuda>5l_cq zi_ii95tHn*q@!PY&xIvyzkSjSARwZNgCt<;Xu)zqE4*K)@huUMjmV~AE3G}46R@Bh zafnElKL=iSMg*xC@FD|)#Ae}vm8bYhlaBI;(MnVJw}|?$&Q|$DS`A1v`t1vw)VHL( z3EXc~kVJ@t(Vd3Be~GZC3GJFXLr3yw3QGfaZW5++#ro~<{d!IRIi4SHsOU5krcpsdW9E3lTfd_q zyQ$E6Y&yoU5?O={_`T2QE&w5C%x0iPqd{~r)MnteLiC-a$Ki=38t%68+UhVI>fLb0 ztPX&W8!5;tAb?B$vGiT#l2}gN6HD1(G;!d5Pp9{>Lc9M&xE^7sx4@at$!8rj%F=4@>Vd zCY=<#JtXV9ql$%^@5d>ZLc9K+J8K6T9}NEbbr>(it^5FjWrDdw@#MgI5b~$3=}Kzu z@y&HgpTBj-Ik@GmY^p+Txr8IjYhFG3ve^ZYo2r^7?Zj8U@?gdWrvPopd;8e(*?O~F z)hw8MP+@_O7k!04e!sesGqx&N2h}Vk-M|(X0gXo=+~fVn8LSW4TsXBUom9m0(r!h3 z5F-vw(cijzCSW9U2^4P-#UNM!24i1cnsEG_#qPk#6o%A_3Qyz0nUIeqxnw;Mg*kN= zJnkc`z3tb2lPx+)Jw-E@)K=hR5xdsA=yY{GX@-?TIsFecYp@kt^d`W9C|}cmlizEV zDT3>@sh@IvnqSj2Z*und8V0ghf3;)N>Ua#i$He5c-MKKz_r{0{%?P)c>PGRSVQ#S%F5|rX{~~u~i}Xbp z&SQstkQot~O2;j+e;dvIIeUA)>bL*j@yC}nAxqIWi3t`4TTg?-WVI|f7|G;~eVXj- zWKgj7E*qI&crim0!5lceH8^ZJ`e_fgjMuM^J9&V^*Eln?7ns#fdzQpXt)4;9m%-W8 zN&{DQk|X>b-+%vvcHzPMy$zu%KNr6Jnzm8@)b~G)P@3kU0Y~{^m8NHvQS5;}Kr`?? zO;3oQ3=svvZvtln4O(8rx-Na-%uF60DYmxuo4i%OYYvbs^YHVxe~n5R-l*H*!MA< zVo?pS1fdgKJ#4!3uXnvszNTfQZuHkm5s$~OFT%JQ;EGi=C)i?Po|OtSe;O4yP^QCD z;FGq8FPO|gKR@ET5*9Ac$tb;2Q3C5c_(b+uxlQK}>KZ$#)7Ra$yHkkrLri>zu)*+u z@G{DiBG_rqDv*Fadknm&{}D}e;C^6gg%_4W=e`cwvGyB%>!U8O?(CTJdVt(W0RW7J z~I=KGm)4ls5A{3NMUVJM$ zRqqNvkKDwdaq&s*89JZns}YMDs)-6KLy^bNmYr?zp}G@6A*;!FC}P)c@&+y{;+cz8 z#rVs4cx`zj-ICU5Zuh+sI{jod%EOEkzvrgrG<-KUHhgAX{WL>&FIp}ye-rWbc}RF1 zM&`GwP5aJl+c6y^l3a7%c?MUCoe6f+^~L-BV9j0;G<=a!AJk$#@xfm#FJJb{)K@sK z2cUlqx>cI;jxyLBorV|(cMI2tOcr)OKdeGluY*ETsP`zCn!3YUTbPdJ!Fy(4DGtIa zHn{_~RfS~o0QMW~Gc>hnJ{WWHsi^4W=~NeOHIL^ol5lhS>**hwkE1dT_~W!&u70>*|J&dx2i2POw*yRRr_-pB6HHCrb^q#e`bSl3<-` z*1^X&3PON^U#0T-M{uIQ_wld0mr4oMn>#J--wmQ_YFJ24IrT1{V&dSi9a(sD;*_11 z#LgDcfduirsA34IG4g%7-vBBD0 zug!k^+RYZ-JXkyaxH9(FoF0zXibG1ze@y?@P zQX|EWW<78GKP~{~V17aRw89Q)`G&!Fp721M>Z%5b(86*Hzm}D0+J=p{eg3;!*rO<5 zVq)XC>3r<$bcJL9_HtI&p98)~F0AwYEBP#G1#;R#gQK9l_1>~Aark|8K6%=Rwb8yp zE6ekZi`B^#?&j{?p;NN^v6gq9+z+vwp;2Xt!tL8#p%+9IE32MlXvW>YPo9u$;f+NV=c`KSJ#IdYJm&?mY+l|&hpJg9jAdW5QMCvs zvCwZzSo`*ZzO%BDkGwYl1gC*`@R*u_4&Tx(DXGb_||&d%>ix9JR`>#7tc574{b!N5Vf9h2zu}Jv2*7x zB&=+Rj+PBi`n2uFa%LrmS~21TjrCx-*VK~id5UH*WgW+ZVsC0v9ZK=3G+aMwYgI@V z0z>4bo4orngm1chZSB;2JU&h;ALa@=(61>g++$CM8E)Kqxi9*+-&QC~B#i#(e+HXw zmb;5zbQd2KqxW4OpXU`3c~tKtBvX>FVjp5^5Mh|CoT0?|J8fmpU`gQRPNUiygO+2I zCr;eT5s}^}dnNBG^S~TfQ~XDdZtOg=;I;ZZk_V`tbsx|HspJ$8+6>-M2OE zC^8bVD`bz#ij0hqS!VXk9+g{)kewtUNkX!-lf6R7&Q94QWc|*!=XrkqxL^179iR90 zzOHl5bxxAdImEc^-Rr3SN9Bt9&3|WGDaBxqS}M-DGh=x6j|9uk&^**jO4uQtD;dq0ktAOz6eAZp{}s_AwKFd5ot^jC0@1Zb6b^7VH7Tm5J;DTE z-ca|5YZj~2m>5p`%=`7d&y=4c`Cm#$!J7zYA`9Ts#MQ`V~Pd zxr~inWE- zRGag21#I&s4l*45KoUs?!^_FXhoW5aVTZ-L$M!n^p^iAPK0R1M?YWQv-g*rq?HJ^V5fzSS)#KsgiHUu9fUi3%3kME!=8uKKI~LivI9+G0M^+vl zJTRIU?Z)R(`o=|y1u>Ozj}O6gIJhBUcJ{!hPc)W-YD~^|i=e{jFQdEV;1IO3!bVGb zfki7frz7PFMdRp?MMwM^EDgF8BmU-5Gb@7%_93sc$uNZ>^B$K#C2HyOYod+W#`+4G zFLg4mq>#^X#?6*Of(nHe{CyG=yIiY}U3DwHt-%qd&DHq%%F~%Rx-(~N8m<|7W*C!2mFxubr=4wyQI)O7f&IuEUSEmUVM!MR4N$?OPzCy$}Q#69+%`c?SlLF-N%1_8+oeU*ydyFRN&8r$T8_NC|{XQhB*QItyV}+o$-vo@>9V zw7mFAGe|+|L(luFab7-&!OH@=x19Lo8K2&=wT%cNuH{ixCT1pa9Ic@NAg-2n1; zp#SKxJRAboC-sLOR2YZGn;RJ+1AwBS*e_X5GEmm|^Qc1?5A!JHVN)F)>($bXp}t+4 zD_OpA3k!bLhgB(yyf^4PJVLJXXbCyL^=_R+a!Q8<{+sOFT$#k{*tQ0r5AVak-}8jd ztllJ~x|-XzUwgDA{@&tupQND;8Pa_^x;9+r>_quFks9RcHW4d1`PskwLtp$0anXme zXRvO4Q$P6D`8zI;?xEDSD912Sy_6V0V}_vJr7qoTCF>J=Iy!z+1|9G1omNzK8D5f6 zO_O#kB9B!>y@NL8F`8DVoVx*yxrvGQfuJ}P_2cN#&~PFWbPJn>0| zetB8Vn8{?wR*p`_Vh!z)T`xpB0~%S5h$C?*;hch8fYiy8F#}hKZ3h-9Ld`G4jdy)z z*!&?7c2$S0e;RH1vxUp-G3iasHqbm+3DSM!Iu~%~&Uv_w;1ywCxVc$CMC26wH!15) zi1Ys1n)T-UIf`Z?GkbKk_Hp$`G*yJALGcYf#3b$a=f$~$K9(&>RtQ>htB^zz;lv3BP#IQ*%&`^j7uH|gnQp|wb0 zOv+JjsqAz45EAmK-VBZkHet!-7zv;sk)qtu6 zjWVA|Z;PD7;y!GY8Amqa$r0P^vhyCUn3j)M4z9Yb(cqlF>X6#uY*=j=&OAg}hK}=mPTgr=+lGydK1;c}o{=4`ypq;qZDw!(w5*J$y3JVN zHy$YTT;z{W0e>vZcgo`s9d9y=cf9a7*v>1Ut&L*UYi4@7Z+TTEyfx+|y{-89tKOz8 zU*bh~r<7a?H<42kdkOmnv9E8}@;mHwmy5kcKU7U=aWycFs;dP1-fEjGy~A$_pZM;q zq2coF?wtxL67hyq$*q&JvNdbltL2T;)0C1+d@838I!<)|vfNi{kYoJqW(s-KLggL) zs<1Qka0PL4io-{8Fu=5JI>vYXG!>PA)ybz!AQQaG{sT^hrI6rYA>~wgb{2QRD1|5u zj?l_i1KHaGabYNtYqPF)q&PPs&5?~aYzLQ}7EMAI#mAOdGWBlm{xKY|K_(-telCui z!}L^XsZxV$X1CQ>%TC-r`T61Rvl`EYDJfBf(ET>Lefvn1Lci0^ArfeZ(B8=f1SQKA z*O>(VS7m`m>{;_CI${~D8-4yvWiUsB#2+OdLmz+FL3fHQ*TBXY!{qC4+k1Na84u!D zC$$}gX%eC-uw`e)$17eM!dO>*HeqY}3@UBepS%L70+#;>T;R)57JDg^LjFN}8GpI7 z+(FUcQ1bC32Kvh3JKvqW(YgBU(D}?xO2AG=^OO}w)Z0gwYzzF*e+k-Uo(bF&Dk<^F z>WOQd21hGjYe^O4v3(7~X6UXbC0s-B@@H>)oR=(@X9}wtb!cxlQr|AU&c-K7i&ZlA zGjTrU{Yf?jje=Y#1LKazL10TUe$-w@ed|)VYz;9j6w!^1x_Cd3I^*``e-2aWoKH09 zL!*nF&R$AUtDfSY(d{zf|8?ii2hzj!XDab;T?>L$AfDC4f`SF_$#y8@GIU{D3-P^@ z=sxeKt;T5@FW23RHnBMZ3mNQB!@208{>>PmHBnKdsI}~BNOR!g+9%Jtgi#B{2M?H9 zn%IIHafJM8S(XML;$o>+!vf za)J1kr9XX~nB(CMj#`Ksbxq1|{AB{WJh_KAfnM@Oa@g0wL4Tc$H`-kDztV?Bi6q@Z zLcPWqNJA&nG*fzyMJQwLOH8TOJoo3VI!-o&)UQ3a{*4OzFxWd3(_v7 zhN5_&F!|7eoDy`8=$>2$Z>sLgN=YGoEDeVsB`vMJ5fSwC-p-E}7Km}5Db!b$ekb+` zt<7nuPDN^5b{kfaY%zJGS=H1WTMUAml7KF>R2|*jU&7AB11Om5k=``O*Jr{*YQhVN z-d=QHcpRbvbCcy)Ud;f2v=<3(SW57mg~_L!YWga5OT#$Kgs9!e!j{l(;J0p=vCAf( zH1M#HwaB^)yD-r%8L_fbl{(v|Eosf~?0<1hHpGj@&5>^%#Z&1p_9!5XOwh^czRs;J z!{EN@Qal;kOTOEosHt0q&UaV7)tp~ZRHmOCsTRCr>3Bg{`1QUWyBL0c!J*@7fH|_; z0;_JDIPoR^w>4B!1cM9K2zJ~@m%^E=l@L=xJp}Uy`~m~J6Y!)OuA(cBR?X1F0D`o- z+DD?-Ilb>7kSm@kj(f!Ud7YW5;IA}w1Q&skd}-CKiOYf|&c4{>G-=`Q@ZZ0!)b{N{ zOV_Kj5A!T@<=5*+QdL$l{>Q#FNL5SYy)e99(O0gQ`Ax^$RP|c2D*J<96J`(c^4_Z1 zBFF-?IM6oV+V*x_EhKGyo)7-~krImE%P&AYvNAZ_sa0M-Z;yqyGw(AWM$PcZfBR37 zQ(r%rUqEMm-Kf7XTNcOc)>i`RT2{KdB2j7v&s$?#vc70Yd3Ltc=HT3x=K-S7R~<8y z|0$%9`)=0mJVp}75TO29H)|(#o*yU!*i_9lG?XIU*OI_67+z_bzUfXgkLYH$^Mf_q2bG>jN z+61PsqsNX>q2okRQl*TfrRg&szJKX~<&C7&)G$DR03Xm|OKdM7wIasfCxYv+PXpESH~i$)?Cko76)3}HW)7r3_ec9Wwiud(0-#$4t6f|QLKSPUPx)ja7M!k7@QGwH6 zqvBue$B=ba>dwp<*@j_I$vc6C9`7_oGfn}kB!X+OiJ?eaSHnu53aiVVW8cs5P zqZ`^#J)TCQ25G(%xv+v{Nqt|`qre=F3%w0^Yib?GWPXZ;?MO?A7m22RathLEjD+a{Q8xeJD7i|;cyyp095OACI}ED#FD2!3os?LLZEYLaBK!1-_3M;N;Dsc%5y!hf zTtc0OKa&(XjIw5Ah#w`+U&LjL{vBq|b`i}~zyZF!cqa4i#-U_YEzaX`l8F=erbll!RnPJKtG{s_dI3 zS-kStug)9$cDKaS_Nq2zH&4G$0I_^Tuk=@nUi1+a!a>7}>>$z0&nY@-?k)bX>NX^` z^^NC-MsU{Hqd0JePW&{89w_1Q>d2i7NC1nd`0NPu_Xq=j952+*QSGY~_sRwfr#%CohXMNhdv z;*Jjwl~kK6sL_;_zLmrGVSp~f&sE%pPFsILM zQkMUD<&4X8Y}SP0 zmiEQ2A#E@*<&9+`w4^e^s+4#`Xw!LPkrU(Z{OMkYdx5GTrGf*RZCY9fu!ncvMxoPV zV2g3_7H@#IFG|73o0omWnV46|EfPKKQIb_BTvNbvaUo0Qn;H{Nv%2}&ySqOSAp7lT z`|@?T1T2hzEtuOL@E!YYxFayMR^y6b^+^X2I%gO9X?hN-@$78Gz(T7e{3g}g+WJeq zSb3_djvajYoptE@4j+hUYI>iO7lk$abCv~uTcv{s zr<}K6?mxZiS8a6b?!_b_WocFVWc9TF^k*Yhv6d{)?1;7o_tu-#ZuY2Uy-&+hmy(Kl zN`GH;a#FXp`DbjsAEXtM6v@25Sw^X4!mb3-&2o&SSm`5x%{e)=SHr)2*Cu+v_`|*q zhUu13cQkBm^>u>?6oh+6Yk$9fTdP^0;j*V-UviBk$L{mwkL^cmj#5z6x`@B&v_vSk z)>>ME$6+9ZqLz7Fd;WT<=IV6vkX=u|F}hHQ?fOogJWo=Yn_t73EtUBnTjyU&VG7lx z7wxJ>frY!2_b0p7ajqECfEE|lK2J;y=waQ#F+q>+5;w$vtG2!0db}&+7%q~Ch7gmN;r?7B``^%J^G-x|cDbK{ul?f(5Z+&0`IzhMhxCYq^(G2jf+7sxm^!Jpf4aJC9acCF5k~G2(m- zEdbPitUM&!=guqw?+4{c2O&jiCdiIT+TU)@SM4qSk~T93N5S(#-y3>zymRw}*27My*c{JaO_U>-S!Twsxbwi@YVJwm{{gqRtQ7KL-d%5Q7xz=`GsUevnOOBE5Q@ zD|)qGnJD`Mqqz(*asIT(?4rB7Vvnt$#M2Eu1DA;QUOg^NNWJmV+V;;oEWLSgyZ8GY zBuGp2+t%)#@xzBLIH>(f-bgZB|rt=H>r}kl6sQGHD=cPJHzkfO~sS6+0%;?~7N?u)YSYsdPY|k@Rc?J5*6Z zUl%?O?3#9_DRMB@{c>TxsC7$BE32#$SY!+;+2Z=CYu8)}CWcS)1t`jwM0e=@E>qc_ zGteU}fM|?QD7hc){e6s2d`2pRia;h|1wmb1qG6mPfW$L$oLpvihNg>}O>tMaynFu* zp4%2?jvph2hqq+P2^Kq-NcSBe^5x(2s@`NI-EfP8MAFNr+gM$_`=Gzt3n2%^q93+4 zc?6VZ&#_>$f`i%_3Y+P^E5~s8ii>~b`>4$YnHUHhbZRNPznk$nj!2VqXJ#NV3CmPJ zUN#oRr?OHTO^d(uUcQ4+#OK#ys4m2s$n|}`LYk~9CHAt)R*+Ys%=3%3Em3DT?~a;A zIyvrn3^k;owbhG@R9H3m+|%9N^y{4-4Vc?MbjQmXG__1u6KY3aC*HPGg%{xu7tys* z$!+7K4{fLaYz&b+kK1sYjGtSKZS{ISw6jHx_?TUrjlSy+02T~4dRcH)*x0z>Np{;_ zw}93G?WN<*mNbfe_Z^EAFlpVr_<7p;9vUCG3Rvg=r)*v`H9c0v;1gM2AEsZM$}Hd1 zN+h3(%~739gOxq@f1QVjRYhYs5l*V^wUQo#P5fQU%lwNytuIHXd$vi&0t00`z?Ubl zr-XC4fSY8vZSs1iVuZ+@c|ZzHZFBQJ1G{G{E6+wuj`^#7pE&?xq4S;CXK{gi*ZW_K zkvjpN9p5B~QzH2Bh{T_b=V4p-v@0um^(YhYe2pypLXb?TWSs1OC9$dLNdhq54H|#q z0Cmi&s8lCut%g_x_WKaP|DYq))Afl-W3|K~)U}NXewtt%fZw>(@w9pt?}ahz z$Ou)viLymUwEy~v1-Fnc%?)qgEIjQ+MM_5x9NXGzF8-5qCGq`>xgiyvEbx3;-K4>0 zk7RS2TK*o--mD`dL90QsTo%p@n=7n=2`{dbcfOM@(yJbR+Irc%Wf|Y2#Mfj0f&B)^ zRUY%dAeZm89!`dNco41%IsfFB3wd!l6Ln5@8fCgNy)5oiPk>5 ztPlBA@c2C=|MmMFe4xP{sT)F``S8b)Mc=>?rR{XE;ifXl$etqV&UarhmrYqP967S+ zU(76jF;NOm#WI)jbck2xep|gssebd{hKkAtsSH`c#;xpGMr(nd{U<`rr>3pp9?fym zlt_O%^ZkEplnwj#IN(&#hpJncpE@b5sMRer<$OY3A^X$wvG3WT^N4hJZTmeQbRGdk z%ekRfnUNgr{Z=E1_dt9-I z{%c36yRr`HGkJEv^QA~u_yh~;SAJW;F$_}i#>Uh5vNJMVWaV6oFI`JH>^i$Ia!!%- zN1Ghc1)95$k}d(y)h+0XdNZP@%+r16<@P=O|I-54Ko?2k-Me5oc;Tg&;|ma2G!SiLDH3U_4%?GEUVhg3 zR}-&K{Fyn{Z%ae~ya3sQA<6LBT3L*j@<6LrFSO&th+4FVX4M)1n#6C_7E(X_`{*7*0f-7O+t@ zI=hGnd*oU{+Jn2>Fcf;TKTVeXZxSmZ(9m|^5V&}gGP{2&GA?#VT!&M>J%p>x52Xwv zV9J+YrIbO7#~>lPS2@2O7~U{`*dS}OIEOCXVYZ)=klli&M z^PO4S7k;pwP-Gu7F$g$2kiMQoL4kY#i}3W1o>p(|{c1b1-c> zgflqwNFdN;{R-b81jsT|HIfIL0h4HW8`WqEk`vw)crMYffU)`7**RaPV|9_{cuKg~ zBCGt@w$sJCf~psJ3VH;J^i(a(HPU5jYWt>sSxQsxPgy62K-**Or(CNlPh7o;iK><7 zdnM9+d+ryi?Q0(Zi)dzY`}8Bof@}rVc(g2oq1uj-3cMvE$$(W@b&mScQnU>CZh1w1 zL6LW|)F8vUxU=(o65FkUAyGa@kgHf??;;uuS#2B}gTsWn*NoLp#Q3<3f!D%*2-Dpq zR*MXc-3sHt6%p9~)7p)(d~LlP)TMaaUEPSKca^D-+^mY3-x&851?L7t4Qv#cI2^@O z%Iv?n<<)GS@Df#^oA8|F>q|K@l zp@Fj?s?8<6^D3AN(nmn>{!?Iq7#np2MM*$lBsZ`ASUyjTyP@2-QZ#~S2LVxfPP{9P zOG{(NU@5?()xFm@q^JK2VBG|xf>k_jYZ*g-PYBws6*`*kzkE-{_wPPJ(}UTVF~->W z?nQL2ioE>Hq5TM>r=jJ>M!n1j8(ZvT-2pQMc>M7Gn;#ZEs`@|WRW3R{!Zr5$w*#z8 z#>crqoY&VY*VK&7-7;lqLWqutT}d{Sx3cI@MjoqfZx8uC3n88)YEV~7T>_JL)ULer zN{U*B@P`KYgvv1#zB)KN!HWqYxROz{kBO}4SqBZ-4R3FP1Wo2e8G_MJR<*!YX>;Iz#OC^552>ke5Vz=O2tnjMs z#~%K1$FPo65~mSq!B8>YSCPj~Ifrq`lcPxQ`czCSdEMo@fUe2y+YxDL7r{hCHaeR1 ze%BWD^Wz4*nzGVv9bL;|oQ=)61Adp2A6-O!ga+e)pQgi{lF9~j4+=$=oJ9zSIOPPcrayy`TO=Oem`Q{ zL+}j1cm{f*tWuW%AlTvi&@ALJ{yl*qGqRB!Z3+gT0ozvvtEHo*i;iWY4A)zpG_l2o z^<~y<@f@9;@LAr&SjY6EL&f(+VxokIC`nt7dnDBT_8*{AR|ZSh_Ev0Uc10E&6$DLQ zN7_x@2$_nRt$E3#Y zC%85TZ5{k@Z42TNS-?%J#?uOUuCvSRxkuA-D19z2eOs>cCpao-?et7+c^-H6u2m$Y zs99)hyAh>2J%wVJ1ZrZK$Y!0Ew?~~up9^#3?lg_D3qxS9vW*8e2g*Gy1HLA8TSbz6 zv(B&kdYzMw&hfT_grNx-IrRvy2++UqB$sD(g7E=}f^cSdI%3WP2>8;|HV`|3@0d2} zCHgBR|H_3ru6;0%5fw;ibsBFtzUTffZf-IOL|^1Zi@>?~nJPV8)>D!NF8DwW4T9spFd8 z@*hm?_}C$YqV&xqEtrIQ*W;R+fpsr%7<~m;bNwYA|6pn9vhARr9OZtnAH7TB_#UBt zP-Gp*Y^qr`Dxf`dfFf^HLrF#D457!EzFca61c`=YFJ}Y>{SF3n06Vd;&7bky`&KQ? zNvnX*Z7$~4`TFXhAb9Da-T>{d4Rvk%X2L=lJ}L+|pkv1-^!qaPGT}cmu{h2GWaM!* z?>%WLzgV@j2k?4WsFnK@%=M3d5%JCWU$j=8=$@9L_p#D4vN)R^2ljg=yft5>k1(v) z-Lw`M75BeaoE4F?jw&b=|HiZY7Ku?d47_Lo)_~e4+gC)kQWvI-i2_8cXP_FyKk$VU z<0%+;*Xde);wGB|e5{|eo(<)-3)OVFd$E7@JLjuMB*P!3-&xtYk{?cilBwalX&y&* zVPSQKF73rIc}Ni~YPHQEPRz(S;3v!O4GBJEKqkD^bZ6{1hB}m$3-9r zZl%LIe&*=y_KaC`6!f)Bb$I9I_Yybds=*>5D?dLzHmvQ=fjFDF>38evjN$vFq$G8A zlfKQP>U*mRsIuswX`ep7(02u=oTxkeQcPjSrYwqVkw$KX$BGB)K!KD?*7WP$o3iLp z`m+$qU}nYX-}W{mDz$Fo-R988U2ioThpOi7Idw8<@^t#Un{#LA-)p=RiYE#>fUA$7 zgZ$G&D|BjcGFsvI#DsvbLsFb>!6pI2)QN)KPUd)%1vWGmRv`JnWVs9TnY1buhncxx zB&lzyf#<6wOUaT!dz+GhyR9p`3SDMIjvR5|qUsh;D5k!4ts5=dS)eWr z;ibo~^E+GRhS$r2bPEJd(A$m!o8Cv?I@=0TlJcDcce#Ifno3d$Ts-(*2L!eZHUDZ>CPne!7{;cq|tDjVQdLca=O=g zQ$6MCNzPxge3~TrPaJ<66|`;<2pYsm&^w8 zzOl$9oEesBu>1j?i$g&~v--`l6G{twNCAMTv6q~rdh@@5?~jagPNG)h+G3~Tq*jQ7scIyuG^ z$KTNjPy*rkfGEzwCJ*v)SeT@CNDIA4g366J)m~)$A&~sqrX~^Ub37kE(VULqB#fW* zwx-ta;{gfnf-IpdE6(sAo;qoF;C}(tU%NhJXICyR9>~Zb65JXra*F3O5BcH3Fiq)o z?)Pt1ydHi1VYxL#4wC)sMVuM@qK~LV z$k*a)g^(7|97qD&p}|)f&D)oHbYW|^vQpfm0w76)L*-gfF!WoPdGfq22*+ezXX|Ji z?k9UcyIOnbKm@;_F2tvhEkZ8F8WBw^*o6@gR{%$kzD`CUL5F;pMW&?Pa-94z3JbD5 zAs>SN4PL8g|IzA2HK0_(red}Kp{J>AW@dx1Cvk1dLYKe4(z_c|Su(r&VgJebzygvE zD^3|ZI|{tE?QJti7yvwIEAnK(SRCp((1G{7E@Wz^)-V3Tc$pLx{B_6v7cHKXcW!1JP>%mKUK4y#kB4H)`Le?s z&2-$yb@dUYr$ojEPhSExfPZrBnl6g!DdIE40a(4h1Te&k^RaBQETz9V-+mG#Y4QER zvnhU!E&tE~LGW2r0i%_xyP=goV7`vP%H0?@VBiZ}-)n*|74*VF{9OO8>;H8I(9`@- zPZ0TJSA+Wm&p!u?xUTNUuNV2hy*apv27)O}u{kOVZYw8IxiC6|jLzc-Dnd0u5>9Xe zXv3CxB|tSG7c7rs9dny{$~o3>v)WhDaENj5-ZNORBP27};eMT(p4RAc@bHL-%fiDE z4$rk=A9`OejUwUf!V}D(oZ5yrFU6+$Ux}qQD3s^Xf;&Vk#aRy&h-9@i@ZGp?2vSh( z<*Vch+bQ+AtH+{<5bVxrbQe&KV^IT6GWO7j12#)H=L2#~cx8Y#`1mSX%+1UWPSJBy zHk7Q59Q`K+ly-F;Epzw6VZ%KLVJS=T0wX4BJd^g?R2*`$y_D2H=Iw*j*rMtw361U1 zUT@k#(*RS_oSUhI4yza{k)=Vr84Pb`oE^K;yw{Olr^GqrE|#p8WN4;?n9pO%Kw4Q5@um15s zdWipXTBRF}zxX&}eWau^`1$K*O6yI^q7RwZn(;loItH*3i*YjjLU+U=Af^vJ0z*%+ zmucgBWi62sA+37LM`Ep;XP0Jrc(@96kSTPvI7%tstD$ur8o~}hvF(*a{_JOZ?RZJ3 z+ih*b%wbP!O)=f#U;xAh4uVqwR23>y+!Q!VDLA|CX02jQQHXelveLd=D1o`YMLJ&D z+wi6#7;@Aq#vnb$r-}rl4#PG7hz+wf97CL6AK#<@1cLAz4eFcu0eIt*OD>&o`}(Cm z7u>pHJfTv9S)K(M8Nrh45j&UFJLdO#Q(a;_FQ=<}hDPZOo-P1P^B?+8ZA4}10S5t1 zs;HC!6=%!2oy8vU+DipJRJ#lE!yAO`Q`AhIbCpiE8R+$+NMk?(al$8BTuI_%A7`RQ zf1B>4#VD4c4`Lz~ieJ3yTcpPl!x^qH4!QHk+}vuu)=hSZ3d{4YP7>3Je1dyjO7%&ZeKwQ8QXodRsLd__BzHow{hNvK`Px}V7UP%ztJetzu}v{sYf(hPuuN*L}-?RYX^ zYl8DkZTJG{!UGtLeM=jp8;NF?$7KiSSDJvTtGaP?1%26b3(N!yNgtSxWJMc4b-E4u z3_VXlU*?ksaf%|FMj!Hb=ORsx@XwcF}L3t zED&e`hK7zAl8b<<+r6uzxCfw)ZZH)t=ckG|a)rS918Yo76c}IP(bpjqw4__&b6JEb z&gbL*?(n*T3i10ck#kh5{WU;~NPM$!Z3};40{suA6P_F`{3;518U~0D4dZ<^dE4Zp z0pmtT+%=qcf_yGN-CqNHM&?5-3}5F$LE|6XH-eF(s>;`U&ntzLcSZfu_{47N^un}n z*#1ORkbacHYZL?sm>}5F=DG$MhBg-jXxJgMya6j2g2IDw3zM-z3U7fO5jw-2OpSMe z2TR2jN}w2vH6Og~oNFW3@=`XLl`>raMqhI2>PQQnyLCZ9K^S@z)-$1of&w}}r+Or{ zuw;A%nE5H?oHF{1sG>U4^L-~@G&gr`xa8RiP7mJQ85qjeN~<_>uH=cr_?2Y$579m8 zS_xQl9!<#3i;|^;{hBJ-6uiG+Ev9=mkSSC`>xg445SgPS1-wGvA*Vi`a}&Fstfbn* z*u8S`E>lPuSsWMq`SZkRk9q2&+VRop{-VnS+0O8;L4a=nyP^Fno0FZP6&!nLS8C8K zCn=IYx|ss^+iG55f~;pnPcqEi^6^o(crZ|Q8CB=}iP;(QGQ)oiSu=FC7#|AtSfQY} zSm62q15^|z=NBW#%a5DQeoQEpf24O*4Y*`Jxoup?5mgaP`?11f&A!%q%M64+e7k^I zFo&CApC(Job{{>BCUx;(veu>x*+ksxNN$8?UoEr~6Kz`Yk3@X=n z>ITyyhmBu15F78c^m}b}uyA3YN-T8}^!G#E8EnWf))rId=g${!=lzO0Ft|m!4}@A3Ch2OO(% z)PatT3g1t3bUxfsuBKJ!0BEab)72E?eG(7|FJ?&z87Ezrhe(AyV}C5@2yn7v; z_b(*R6(1B77{MTfJyULOmjefM&|>CYIpl7#!dNLIVarIcp}H9Hf@rJ`EUl5(8lf!yI-K9hcrDjE_HM z@_1NfvAEt&)I#JXNJpnUF)4^uRGc~BDNvJMIW0_!^0?3o)|8v#G^8J#?iw+OVOj8#nW^oPu#k zjBKCFL8tqdUR;Zb2IYuj8bDkW`(nCx>7g!g0rbl}26`qw2HW(VmOb@8STEt`78pMe zcFJ2dqgvw&WEqK?LEvZ^Wk)y8#r`ABMSPv!{*QJ|T@D8vbod1kQC%2*mDdE*QWrT* z+K?Y&C|{u6`LBG*RiYANDwJ<~p=KxuGBRF|X!ZqR_~`AWk{A*T*La;aHT$eD-|DtU zZ|%rPm*?V^2pX%cs3PS*I$Ckfuk=pHB;S0m2^&n0S|{j8L5F=PqdybdMBppi`fkUU3}L+oS#R^)RpIuS{&v{dK;V+-BTtE5y zorX!GEeP&EE<_L~osnN$eAhX0f2O!Eg^+1jy9E0*-x%=B`2c)HhAL=E|dYWt!6ueao?>hPS_h@ zr4KU-%uDNE%Rx6G@5o*W+yyr|+KgA74a$XpS!rprby#Ec2GPx1Cg=apc8)}A6$uG; zi2(fMy}2=PpQqhfq?I$PpA`?X{5-@Os2e?%m4L*ERJj@g6$Lt4fK5+>a%v z2>r$t|5j?22zJOk$YhE(Cm0X=w}+g{^E~tr96Jt_xrIfLf{c-KZoq^YT?|j@OqaLEZvu12QNP#Wtpop&>(83?=)%-MH+kjr=n*u*uGQp*C>4 zUs);j(^*tMw6XEX`}gl1M!v9D3rE9JH+2aQ`Zr65ZU=PRMRyxR*$zWC>^;=D+p(xF z=-gnES{h*;%G=x1y!a-U2hLIb=FW7f>(f)K-V<9>^};hvEfX)YwYm0?#a3`V&NXTL z$aV80{y68g8m+(b>;1dc*}ScHgPev>x#L%!N1N)3T-UTfc0+ z?c&(Nx3%5IcZ}=9kv+`TJ(=4YiG|N{#mlEGJ|y^*wMMjbe7yw_94=@^a9e6H4Ld?j zHFi|?GO4zq3S*!eWks=tBhge2qIm58E}ycpa_F}`!Wt1-Epq`RLs0MXIJA~_! z9cDoQG6XF&v@>y^SnW)Vj0m7Z#idcY9Z@w}>eclJ$XyZ<9oV-goT4;G-Erl4v)bkB z&GFu&tL|{SQ^V*6Uw{$5UE?c;z&)Du`HFpr{V~>p=^35%Lnhe?QE) z3of4LNDrsr;r!Tt%ikq2n$f7uv2<#2v*{M+ZZ$Bw~>i znetdGLk0oC3zsTF*4wVBg)^#w-~|=lO!Y2>-E!aMOIh7XZEr8cE@QN880Q5~Qy0tU z1aEiTT2)otNcSX2Y9z#U2j=$U>+bjUWsuzHdCg@jxwC2UsaPD01{v|SGa0($4>B@k zVQ2Qh`O_tiC@D&~6BEI%?Xp)HBi`|Kt+PchdGxez%?-A2%aFz^`*m9xo0;kJJcQO$ znn|s#8lU{2!JLguWKR$2E9nRnYQQV1>JuUu6 zw1nLkO|A0}EdSNj*G*BopO!}PK12UUf$v7n*pixV9vqETHsCAl_-9&-v1l!XE)FDh zOZmEBz|?GJtx3BoL9(?3by##vvR-k1!*aj&nwuwWoc4nY5bgIW7cNF zoc+)1lJud+VO5D$<){XV&}+x0!X8B)KYZoJJt|>e1IsVsZXYH?s_SsNIJ5Ll$4B>C z!kp*RjF4~Qbt4;YlT)oV%-`VuP$)_x?d0n1%N#>#|qg!y34Y?(Bw?R&f zCs=o`r=$Ms^^S)}tiUwJ#@pG7)>=>g_gI&l`SpuydHEYkIk*sb zL;@Ow-LT31I}q0ZDD%XFyyL&UeL3HbVf;rW(K<}+?uCTJLw$xYD(j&^1;d0@{mG3| zbwrw=n1CS=dV!72AztznHTlox*Mih}4yFZb# z2vdn`^2gGCWaz?fs-Vd{vVkHRs>Ool8n|*GQvBC%8(;})X?x%}wX!0eXQNe+b`=zz zgyB7WYjB-~ZjGd{@Q#xV_WIv58Xv0nC&jsH0K**1fUHbJpg`ZzAdR9o2u`&U70k+7` zlSi=r@NxRPVv6I9VFO0eM-im+DvPg+1 z3F5qR_vs7lepwUzD{j`vHUq}Z5p{|6%%O5*fUK+t!4pG+Lr%ib$mI1(h@N#4o8$6N zO5CW}3=fqoo3W`@goz|RES%Yg51$JNk*#=e@7)`G^w>pYbd7)ZmjO^95U1*Lh&K3> zsAI9sLuWE}0t8Pkj|tM$t?(w5;~BCvhgB^fQ@AtnNRt&!=`@tYVR=}lD0ovg(GCHY z98vHx;0~P|WWRftii+xW5LlsL=_@oD8F?3Uxenl3flIKC?r*}J)9jChNn{EPEzBM9 zSL=+PxyW%GxS$8Z!3G%%<(Mph3XOa@l;Bm_WYm^pqstdSjD^hcO6#&MhfcVhpTA1| z36|1?of4-Z?~S$j0Ll9Mdr2I2wnEPf7F1v=sjDK9bk?#;d4-?(b3`(`2nYJQTkX#k zoxdBUc<42?Qc8+py$1hKoXMS)@@;n5pV%1*7`6(wTVmhyHtZ7c;RXEp;~6)%s*DIa zp;NL(*<2`@f_bspqKUl$1x){MT5W z+)9yKCIxp?q-!BW=iQ#j<-gwE2A@8OJxVIH`18Ey{p9pbc`mHkmCSjIex2gqw?`gk z%C-fjjoCbxNl!2s5(p~(ZzZQ!?fF2>Yo?QBbwAWhkB+<0-8~8p<>-MK?mZeAsw?-2 z{@$@bPZ)(jpZ6KnxGle1*NAec7wMuB|Nec*{RzqIwsD;gJH0wjLWl)nxVy6;X#k=U zwh7R7$P{sfD^8$RAcA8*xQk0*M8;2MYafVLuByArX^<6<#m1Ct*LGn=nav<87|$IZ z?pk`FIMPPv1~a(_qVV8g_{S>+7ZnwV<;5UvE+wvLSen{AKl^iOKX`jY z{LF3_yZ0BAmLevZ1~u-qT~I6aEhD3hPuuLjetWJr4{e_3@Cki8zvs6xtO?(U$ zqZZ7uO<2W#*W4rnFF$D1HMNWn?!mxcQ=>@;Sy1s={K~x%kCK(yas{RXVPTqx3P;!3 zFs^WSS@>M>^gq9H%qo}gvhLrn;r9Ple46y?{9@zi9sXm^cW1`X>5U9+6_d1eO$;ud zNYzX~AG<6RKk+X4J6rMIjwvxXY~|;RAh*Z=fySb`Q@X7zw;WKPrI)ML<;m89VC{#_ z&45s}?U)vU>mQe}8WNU4K@)I4puq7ze7 zU!v;PzlLWE9uIH@)ko0WqP01L__ZKYtna|e)ctqH$%%>AbC&w?{+*q0{Eq(f=To1E zwxCo>xI_&6T^Ylh6;f0$UeHSkchsvI=I4ltJn25u%VC*kd989&Hc&>sB2=~m=@mGx zdx<X-}EPRP&c$xXwq+}WNY6hxaKhmPoay(#fArccEILINWQ9;`RTBypC{v< zpZd8%Tm3OAiY+3-N1E(mK-|=9^kQlIlvk%kYI13*ZR4u2cqv?RlM1>$Yp=XOkIr?V z#DMmIUp`0D)#RX9#mg=N_n9~QpOtog8>^U!9#rRwzU^>9ku@TQZ)W!R14hzNb9l|r zHdyHKSZpGAje_{v*?z;#k#wv!*h?m{1?Sg}-UNe^u=eHj#AFpN!hiT#FrL)1K^~&% z?(RM`@bGU?Nz;h-Th`V6{SVMQiH7);gc!6moBMLyA4(gAEi7E~8l@d#rqoGIHGg_W zj`d&wi&o2U)n3@Wqy0om@jcn(k*|pc>swPRI`lqn_mKB2mZm`;*L)R@pka0WUClk; z#&F{!IA(YKf{0lE=YG9M_-JsG(EhoQrhV&Jse$9pz}ly{;0=0_Z=Q{XMoeMYEM}Kh zeVvgJf~N!CurRwbwc&1UiI*C4q+vZPg|1yK?NOdem>m5{le-tuQ;aQ|$}|3&c3!HX zrC45eE1HwA5La{4&C4+b54aZH@VAIn<+Vm8LNa z<9sOgXmD6i!6;OYI?dl&r7IqNz4$74wt~;xEd9!}m(Dum=exTp;>?ILGpg5mT3L6y zjfGtnwp&)do}Oa;#rczcqqz@g+{bX#OH0qOhed;v(`hcMv=b)Y;>sy%l|~mzGIp_I zjJ=GDuekUn9CG91ZGjn~&}5grd>b-yEx48O&^-3yk$|D3r0g;`d#gK>aCHNw*Hh+T zqM_9tN7hTOPCfU)x3Nek%{g>dm-smjjU$pAi&s^*!_U>>aq2l{d;Shw&5$`{7ngQ6 zP>mn!D}2Cpn#`v5RAU0bx+n^!R_1#cVjack1yLFP50#ayNRI=}zdOCtzI$U5Aye04 zn9t61rppOC{{-XxJGypi<$zb21B4s1w%L?~pKTeqHXkPZtnLe{uBf==C+0$ZSYA1| zzCQgZv2EY{cRQb@^vyqZzBCzs28$~Ox&OI#DvIa>kPBCy6l~6-*q8WRToV)*&f??h z*E=pQpK$t66og(dk=KJe0a@FL%}3Y9;q$Ny6h*K9N#w}hz1=D$TRv$`uQD=hr6vq` zV@JnhvaWMPvB$uNf+KI2M2;kpC z_m5pjJg%`#z>DC{sIkA|!1~rR#ZQ)^S!lbJ;2m}Q^b>eT&8+e;oR8ziYj!Bg3<|=e z@crkl+oxdhFNh(a!C_N>#Gy=T8Y@da6;^6Vtk@m;s#*wAk2J(2N6S5IXgyPoVORny`V}uo%*hERxz3Z*Z-`F?ecbrM-XpwHg^PDtv0V z;3+74fEt`WrL4*oeIYiNHt@bc)m-S(=OdG`v4?hj?e4B1lK2o*BfC-TC;(T$OauMf zz-+W)Qh%wHTogTjN*U{shzY>jaox{5oO}1Wc30RG(4M5#Wr=F&7&HjzDamt!T|5RE z)ORoy$b!9zbz$NFc>}o&P4c;Xj~(${*i!fhpV=3Y zTRJ>JKx=yP(&N21KHaf)E*ZV@UYn~52m4V{9e^0ULVL2%RQiHGr$*zLXsWMMx7^&; z=(c$Fj;+|cHMb+6}{ne-zICwv#E4adGF zZcX>(Y)qCG>0%pRDBkR_KRIrGn`Qf*{-%PPe(?*W93bkW6|fvbB3qG>e{~E zirC+Ppe?e1!=?FU4t|DyQAa9J$yt`V(_!_E%u0b*3{-iXoQzl!x#`5BI zjvELesU7EaT6`TEM4d9HWtYX=d;Bnq&$!l~hLl6la}j7BU42@|S?n<&rfRXk*E9F8 z`?V`}t_95fUyNY>qBDh@+@Yb0d>bV^uN+Z_A24djb+O$4yEtyzGoGS$sFVEE<-u;0 zx8c0*%=*!H@GEHA-qM}q9puIrfOCxIb$h$!*zBbU`3B-9vy>7kv<2R!$K#p{O>^co zkq!wi1Kx)OoAUfTzqOi4Tvy}oJ1EY2b#mbX7eAp0rpwOZJoRA~fNz+(0m%pyK1Im- z`>eBUR4G>yHfDdn&g*0m9H2VPB1Zq4yH?=x(~ur1DMCUzs3)GvXf$ zNZDp;`poYs)4qp&s8%N@AIZi9`zXOQf3p-xaY!!7HA@5BiOdQ&x5I=GjzL}6s7-3o zkR9|oVO7e9dBh3~$1Ux>o5otaPz@vPzk{rS9&?463-dc$^KqSJTfYnb+k|BT$pfCn92KCOr4|fAieEGVqALO^BN?D0~XZ%p6E|`DGrg zF$a+?1w{pVQXEim$u++1rj-V*@H5F$Jp{@Sr75P=N=uQZ7zH(@A{#PXQ_pYS`}v$& zYlijq1lH8K^k1#OFmXVWEN__4_d+{6)S|~l;Z1;9sqaPy(bhwK9*YSXoFlLZz?Dry za~1#w$PU??x{vOej=qglubIxhpG);7T5|;P0C$?ABjS~qaGC$g&kRYTr$h&?CvWTW zZ<+gh0F%Gi9mNo(Y8aS}xpE61LrIR8-_c2in#XQ=iWC+h$Pt;&48+VSv99%}prfw3 zok?p>^UtA0sJL|sW%1XW@va;GU7iSaMdq;WKSTbK7MeHBl%74t`=5OF04Z=hP};IK zo$Y+DpICfSj-u%yZ5=slL~o&p^R2oG2Mnq^5Xl;rTe!N0ISNRNd%N7exX}YN^ML(JczUWIb@QdY_`ZE>#3g-4+IO0wKHnIoio zPPE7QT|q`4a%o1h?miW!GJ#DYDyE)%UaM2{QprotZb@PR+ZWO4H8$`4S+c$A^ba{L41$FCr!agS$ZzRIe7N_6QG# z#1@}}x_8s{4IU}L>6u?GOxFiZGb3ur5s%r@rr2C3b*MzUe#+Dj^XxqqVSNvpZ>(7S zn{|0uZvmQJzovh5V?f8)+&pamxtVVdegZ-KHgRy!UgI=Xw{vJtQ4tzOAaGh0W7+3P zh>e?@P}0Uk1z3|l`P$$Y`e=V}{X-;P;z~ggqQvBIJImK6UOaigzIR0oS~l?j85I4% zz45K8^UK_xD@anM=XLpE!sMj0%Nb<20KxldiU_YDY|iT=BW5VeP&FD^um%ux7@uP> z#BmL2pcvxL$+-aOG1dx5ysTpCOF`dl*A`FUhD2uUNPSqTlLf&1RvKey73D!{u zOk_MBq?kPYn%I(pT;V;$t&AIa7LA9F@BrMsr5{fNm+JR-UrSpPd&1=D1z42%VBGxA4jE;9rYQCduZBp!hNhQ8A30eP{XYP)(n$=D{XBfaD^B>e!olz zgq3gfbzV7st*@56mg^;JO%A0syk%>|J^4GdXXSXQ8mkYIQ~4jsFFF{W7CRBUbA85s zcVAzhW@&K)hDN|>^&(Nse{Jm;q4YR`bMqu1VIYiKTmJ3DF%D%rc(fx6l?UMHc|e|= zQ2iX{wi&z$Nr&ZG=ilg37%@eza?O4wnVN5r?BBdlrPXG2jr*LJ=6{Xnj^qC?ZjI58 zy}z4;?)o(aC0GD(HikjvW5JPL zbKY`c?FpU;+HTQwS20{R-Y=q3&674UI69?aQnpyKVZVv8e=j3`gV3EHw7VaU!G-hN({}q9| zZ{dQBDiI(7*mT>{5oK^wxA?W-C!L&G$xt0>44aISb{(E@2 zd$e<5V?>qZIqMSX)d&GW9cb3&yaTo!*uticV>PFmFRdHo$ zt`$+KN+KN{dVu+mYjv>QUT>H1V=M*~ETxIu1(})QD`h*U`**gJPt)9Z(LO~V%Rc;XbXT3aiNpLxJVB9Wt!-HhsC2y%zC1G;WH9i3TK;sF()@hBt(h@HO zMY5MVdWMCa653usQXdiMyM{)|;SOar4Oq;+qUHnB)v0qXUX=wxIb^0xSBR!hI-oFx ztxRU;_lqUvy0-NzrW_JIk&P4sL#BPFF{_DqImB%cd+tG~0^mwBGfuz-=)T9?kdvXV zu8BWl<3fCQl{>d-SP$chV)#@Jt_r4CCM6OVZP_&ZDRqj^??g9Bna$Ws2b7ARE74X0 z7fdvo`;o7Xi0Nc1->R`t)M$6^Ni_~CYV$Zv3cO!xp*elM3Z(ZO#k8UZ5=6{8C~jV( zM(y@gvk56LMkuBB3$0RaZqExHv98g7T(;s8i180ds7;)mi_sL9u2!ks={>;kf4%_7 zj2m$A_06V#jky+Bf}=qNYNS^apj>B|N>C%-&pG`oCpqN#^Xq28F)@d;KhqiV&GmHp zpC^gtzjQJI%L3EKPIHBAScXcS^4&Y<+#bGqO(9t@HMs!6C)eZNf9o;x(TTX#l4H+S z&F1N5{pNGAf2){mMA$}Cp>j>Xem+PbCR7k%%=I|yy1RSP`}O2;@4l7$OW*80ToVC^ z8e6bdkB(B|?EwV}I2#k}bqz&G!|d-vfe?e8h+ z4tGY27yh-#r`p`0sJC8-8znyEr2u4JRnza!({q{z4^ko!?pxM2u;c4cyf3(5lWSnD zX=gYmcFO}4maV&ixOY^kAp6!mGh1o-84$>G=wdeWX;5T}6|LA-%V+GFOGu0pS zyT|9|oCy<{BCU&gOtb{hr=m=deEO%5Bm6ohbSd?UBnU?$r`ekuDETH4pvDLB;v-uN6@7Z7A<-wv^U=0D%<4dLBNoL>m~)SRq}5% zM;`{P| z9*eH*t`#xV((k?3L3;&wd2!emW%1KT=r+F%ILxq!wx~?Vv&X^^!V`r50AxT#h+YrA zZsqTW+~#|1YKG;mL=cz})EKwD*KuKg#O+@zm$B=QblK;qr3p%e#H2x>@DvJtOoVh! zj3E_oZ1%dn`3I&$<$tUXl98FA^KhTeKWl(~v(PP1iHVGa?gMci6i*{T;``Qq)f_>( zA`oAI#W>?2{zYUSYFhMX_)MxS>xUzvct;*eWqeeXQUBJdU8LM-jf@24x9-lNU-(T% zyG_HM<-UJU50w)P;ZoX7ubh^Ae8fuu0vwR@IJ=~HG{6w+bD7Rd=(apiLiFM8uK_d` zD9`bwa7JG|Avv<#4X?%R>l9{rw)>TtXffNVBz0mK`z}kB!dG-BugjAg2E049bA3mD zbaY%8peI3O+WbhE$~GcST@a8ZOb#UNp7{1R7WSGS50nJfM*r+<(KxmTbIXN%wU17d zCSvj|L+t9Qte!hyM2I~N+Pz9R>obx0E?5eZydP37SWmSKjeqsDTwkYxljY)zKZ(-e zB;I5{I~@jEy88?IT7FX<@qU_=L;#4#L4bjpx1GG}udZ0!!WeeG?=tt$Hc;hF_tf*S z*yZG+16-ZYBPByC@29=IJA`?fvbN`R^C(*c}(DC^3esQ!r+q)8*G@BA&>U<>s=Vl{Gj; zjVTP*Q~SPwD_UA}hglPNq3=XOsXPDm8yJ@JGIcR|426kz!BcKYp9G8mHEGr*`%4VCf6}U7iGx!4_ z%+Z=Ptu&AT~P(g^_X%<*A!P&J_(G=r?@5|6K`B1ai(T8`y)hp0rlh?%5R4nlX z)YTs0?5;!i2zRC7@zMG1Iaa{+@C5ynXO_`!ijrEe0{z7RW)o8~^KHf(s3Bd@Sx=ny z@8RCB%O4XSC5)~N+9aUZbv|@;nIA>wQx4=Qq!P$93^M?D>#|U?+sXZW4Y4(wP!}9Z zyGPY2le`3@?MX4qpJUiyGcLrFkFPY zz7Lhqg-Nyj{8>EdW?vMBMkFy~?#n62P{Cwf5jsap6?SXg#d*p~L!FTArWot|3Z*SM z$Ug*LO2m|^+Y6__8uH08Lzw$(uk^M?SLK4g#+q)wRq)_2vc8(-QOfk>*Y zO|dNX8@@5vIf>-VgV6&!rCi%Ry)&V5c}r^wx&CVP05}&GxYYzcs~^s0RCJ-i6A>1e zLMPQCU(*}?v)Fa?`yTJ3M=^1Q<1(qJ=NmA9U*o438@4Ihx?nLTx z++hj>GlRPs(Y$Ym2g}dGvxn=vTP!lE%Z431R)X%-I9XMjcP!21oSQ(9E$M>eKJ4Xc5KS&}NI|$en*icSH6YSRbqG5*aONK7E8rF6EOkqmk zh(Md~(H$tY#AOfNEKKL|OYY}HppZcH`kUCJoY#J-((HBz5*^F_ z6<})pEo)#IK$5&QzuPgSCiml^UV29>=I8iUubpv+r$6+=ahf^*C_nB zHxUPrRGM)2)jtoM9&&-<8Ek>B(O+DhUcv&i-ISnzb{Oq%d!V$)3D3UgQkQz$;u!yH z@nYG1h*AiG9P8$kLPOK0)Sml1(}wbCgk^(wJgvg7maIyg;sjTIYL%jo4+ijzT1s*Usp;++wTIVaY%_+H`5tq7f*J+(ywlSCrYfEMD-(f1 zd1HMop(Uri{Y~^8_1P7L- zGdIuXEsn8!Jdtx9wgQfe7^~9t_V}OeYH;m1e?s!!fZ{F~t$)3_8{R^~k40$3LHg-& zLKMVjQ&9s*!fLGujVsQs6s0GctHjnII;!;4w%ImE8T5%h=SvU7zkR!YxX=KE)g@p4 zl9Iy^gMe27UgOJaRKR3_8LI|*gwuc+?T`?sjme~pL3Dw(7 zce__@!r0$QzUrDzl6_=v8lh%y+*#p`VztKE0qW%@)yhCgQ4CD^Kn{c0>0fYwD!8>z zOAf_E9OH?X35E{37L_-SNx&7C7;xsekukT>4~$2q2-06TxP{irs%z1Wf!y~}>tw}Y zwo>AVhUHkZ4W~PFrN_ghP)gQTL{-JrOmYn^Y*GMnek8axxugGW#I4lCoQD#E^WP6! z3l`EcwX5dk1{YL6B;IgBMGL*;|M2T*6fk0ft_rgbf$k3xMB{}w`;s9KbQCtBhNMM;m{e)!INm&TxrKOM4({F;A`lQM;Fo(Qo zB8YB*%THV%;FCsJh$Zi0-~8JVunhHjim-^DolTwUEkR2y_}L11XK=#XOieW?F$Mp!8HsGKe0<0$YASnAnV0s8?(I7NQ}vxV#cc_){n)V~S+UDi;6F4q)N zkB=Ra5~a9vC-`ws=R_5#5)Tic;By~bKKXUA6rS6^kBZlCR_@Lk!qRNe%VIRlSZ+Ty z%6kmA9yhXWZN^ALQIlvIpYHS&L3==WRN~_ha$y>F#{90JHipVTX!ovaok=XA8M8Hf zTA*^KExk{f<#oQRWqKjc3CW8-ahq$Ajlr)H+dsf`mF~pp;f0Tzkw!U`@huhMrGE$3 z#&otBd7_^3_B)304hU2r4ldkaWd;Sp>ed0UshHP(hR8H1oN--ZHhpsDAsLB;r~u^R zf^P+vf7)+D883A$dVI824e(*inY;BiJ^A!9HJ+Jl!OSKuvyBQk7c@6B$xr{#1w zPUsmr-$be#+uHhL(T-(y7wO^ufrpC z;Kl!lWy9zK2*Fg9J{SEFZ<+S_tEuAiyryc}R%zr)K zZmw6-{crrd^>~9uw7{Ao?D{V|yEqV`wESSNNBz=(#9I;WBN}x~|E1eblRO!Ho4WV2 zoLnXx$|1(Y^eY{O@7XXZ&qA%KS%OP={q z7=eNU#coE1vlGi-ZCDUtcBZv$q{NH?8OW#_N9;9@iPnZu_$M9k3VuBL0sis?1k>39 z$>zvtx_7O1*!m6bntLloP_#D2Zcj2}7)MN813W*vf5?A83$E0%=0h^amz=H;BoY|; zYCR|R4z#r}-~zFYMIV-rqu&;{4Y=?9LIr1H+5blbI%KZ_&H-`(O}2wjN20dFd5RVW zqgLl%#oc8b0=O;k2@(VVe}bDsB?3zgj0i(UrCU284OW6+?@JABXiu&M&d@%sW*9y0 z)nvoa)bIgIufwaL1V`Ti4E=0lC0SV6YfpAmEF1Ju3TZ6z0QY8EN zQf)32>BDHah`6W@Pc${|RzL{x9!JLlQTKEL4~XLt2LOi0Qrd~d1N>S#l*KXogvW_u zKHq)%!V#LOlB$x4rM=hOHL1epfB$*cZibfas(?q{9)m6slI^E~FBvhyhPO-*Bx z==aAlF4i%tfV_PT&m!EgyU+cq`f)d8)9D(u5ftJrDWjpiOap&o6qv(J%+la;8G-wG zsvrx#!$|vj*OD^U@+e;ZL=5xEAUraTH!ygt1F4*PS%_c}1*}Ri_GiNk63RqcA?rc+ z6~{=E1Hc6D3hf;sOMQCjia>7zn5dEx?bqSR95KOv`1@C4Zd_d~RyMiZG>rBTlEmpl zj-UW5ayb^5{4!j7U64VRIx#mlD$s34@!=!M8Uh5%AlC#%*u9nXhnl%P{vd*}zQ!9O z)Mo6-fSN;bw`jX3zvl_$C_@~8Q+x)_)y(FZTPGoSuE#<&ElkP?Jv-)5__f}tve?Z0 zA_vOJhvOjN4}-_zZM5#4PD5Tx$p5SQ^@|Dg2kGA0lQGTV-wnnwCH`F9&6x7DC0(ps zyt4P&YK_NfIFub;NuIIt_!?KKTP7{~ZECI{$~B7A1`*LFRHT z#-Z~!v|NN?{_GQm8~>CE>lU04feMJ-H1Mmc;#e6N$RGRFUxWk1Znj%$xpJUxLU5() za9J`U6ASJ5W5)CJ?*05F8_}zbuICJSC?7f5xa+HQWYYTyy9Nc5*ln1g02tp()tM_1C zuNo(G^heewDLX6F8~vQlCCJ~V(qe~>ZVx(b51pf980jo?kG^U=zPcDLlw~OWiP49^ zH^&S(y@4nN@;{tAO)(k@7cod3^d`7HAi()IU)KK& zF0*LX^6T2aNcGL3tyT-d0jg? zWfAj2EFM0E>^jjYTjo5|^97y-Wo1E$iHzVT3*8*9ns#}D-h%W!IoXru`aXa)^`{-M z$r0N7A8U(nSt{NFbF+Cj-FHENdwWF8Gr7)L=epn>4GuzFD8F~_%p$b}|&7n&W zkuH|;!9)tJEACgkdQjqkTcPg29FKcoRn;HX9`~gU$OV(jbZk{gD+EhdN>qN&xer=& zP8p4l%Pzp6s+^T3-s=h7c4M=*foFc+z~n>IH9{gZ-??Rb)Ue>(03$XS)v7(IxwJr*@X3BT@oxXkUR(q}gYZE>A{|{`@f)fB7neS1y^~LB)FpS${x&}sQ z;5&;~N-k=ae5oK1nMoaZ2ur`?pRTa@6>Da2@J$+mYD8Iwx!ibGOuJ# zWG|ORcTbWm>3z2r+j32|54@y(T%qpj~?ZT=GR{5 z8(z@R*<+jAvf4Nhj_Jvy>RY0s2hKKAU%h&vkR;(_g9!dLEbJ1l5iv1=Lr0pfaE%(d z_6Bx+48OfJJ$-2xQ%G%a<2Zz4b}d!Xb(H|chya{$tEQcKl>&baPC}m^YPGs%XLc(?SQsN-vNoe=Q{bH zR1Co-Sq}P@Z{IAl)oROs{TLXX4o#6;1`5tDuUj7E)y%=9lV8lFd`^89(P<`*t*oz;+$b`_+Qe&W0` ze1Y6&X*qM}Nkqi4tu3}_+4(0dCEe-=p>^1WIfi+sX8P^9`6EHK1`}sISQi-|X=v=V zuo(N<*+WrM7VEm`{X*}Eu&P^IHY1{$+(N_J5#B=S-=wWGC9h#7oOUnAMDg1bg`@K1 zlJsp$HKIO_0SDNe7cjV3{9(e}*N{=e*bO)aAko=D z-MG5DR;{ypYLG$5DIn8r_Vi7IzAud!^o#%4P+9rR!}Eaw&-7(h?qj}}Qd3^NEfb-I z8`n_Lz>TK2H)hTIi*4iMZ(X>nVP>eS`#g*D95zuWxu?kMJ~v^1Vp=-Jk`D}Z|P zQaIV<)Cc;gu*&fc<^71?uCBh~{# z?*_JXCt7FEIT>6x%5it6Q)McsyABb1~CJR+imj03Z{JFPiYGSs+D|u~jzYvi`VWvWK`C{T(<+LCr zCTSO!swYP;(Wzt*<_wo_D4|Y}jiGv2XS4D_zh1Goi!;cL3Eq3%)?H zc=fxgwicc*Ysg^5EXjX8?I5i#VH0jLB)rjS7I}8Dc;zF5WoqQ4yM$jw znxjHq`iD33IX^t4)*vD#Y)?gPEm2rlVj3G&;p@~?qKW3fAQ_`iJ&nEg8e1P0=h4QA z#mib!h1rh3)?PKvQ3F^X7$R$o8(;tmlRlX{{tH;JSMvu-LktwC8jCwxMr`-KtWrR^ zNKR^Ui0zagUfZeD3mn`h-<9^~4WD|*6c)651{DtipMVh&KIWt850ozj6n?lqZf%WQ zd)}Z?^XpBxqX2}@TS=ncXD;yheZ~a{QVO@wUB-Z(8{#alD|KI0->I)}^b2sV9TV~K zUB)Z4Eu_v;_1T!8PkMjfM!6%5Sz+4HE@yYVyfnwDi^pE|z9BNo3Fua5ZfyLwcf)S| zbI3#Wy1{JKLk3sWl9FzvD3OYZ-*7BBPV9Ng-`}^2=P`a)pQYZ`x_WT!OtE#U-h&4e z^5j|S{@IQXaw0IU!HR_`=DJ3$(Nz)J<504m|5Hhp ze88J*>2L=v+OEc^#Gr21q!FxD8JX3!IfuaA!Jmpu6nDpwU>@u8+!wRMK!`4o1at6m z4l$ha%W^N_s=I#u3??`04RUDBI+Rcpy%baY*`ZERV2FXH_I3&kAz?E_%d#sf?z;8< zTGvMjQBh9VR*3PKrx@ZMzsuM(I0&P$=*Y-cL$4e0^5XO%Ay=329z1XQb#?LK@B8^l z@VSPw-D&B$-uV56_sf?=IXUoVk*5uM);nj;HT6lAF5K!-t{2XO?(RCA5lOl2U?MP~hihn*5zosmB?oaQTH;2+rK%ev*q9&pJEP zN4!1K5Nk}w@XNH<6M>}t+LZWk85tkWxUU9FB#d#cA3igM^5)RtLO6#~he?%dIi!~l zF=~g>oI<-RasnSrAr-c7KA2V{(OGY`1M-lJ=$6Q~WN~DM#0Ce`@>qV1&+G8f&ctO& zMj~SJfN>O|%*B0bUzBky2N8Vrt49T;=cIDYyx#zI^#$ zVq){Xh19;tNig=VX?0WnapNm$YAh=o@k=%Kk85kcx}G(A*57ZkzOH#KM#YZat@QU3 z*IVdr8ouMKAZPrOCS3miPm8szc;2b)dj0$;5Ja3;$to;)>xrYJ649kmn3}Rs^R?E@ z%uve-*hFHLj@w0ALJ?|^tyws%=gj}&Jjug{N_Q3Y3sb#bLIt1I()8z(hUIQ8sowSV z&a(23beFCI8{bW=Nzkj@`rc+#!~w*soWfz4eTA&+BUkmxbXlpt zV)ApRwJ}U~u!!OhW(`mxp46ZU}w z)2ve@Uc&{N!o<5$Q{ZrTKR*frf8XMrt)AbLr}rn+d}vDzf4EbAZ~r^&>&8f_w@yjo zmU>=^G>^|8XkW{^GwDu?iPU>xUN}R>BaEF#4}B+G+p42cG74u67|G#^iy82tFqM6b zmxII_n`2{jt$#FHh3UWdXd}*OAc_Hqk!GBTY?}kDIqMlR%<{`@`OrW z=7NvOU+3$})BFlcS;38s_Wc|EjjkI#H7OsML=zQ$v|TOVSrZZ;lb;42#S|8-qCvzl4D?FRO=5+v5FF!j8Uw)Z6p^C|3zo+duxla!EyL2kZ%ij`kZ!dIcZw#k9G%bns8QNAJdFEtBkxqZV!$!iT&I!POx8tJ zWGFssdN(!nP2>8>gXxR$lgxYv;I1M=zcZ$BL0$g+vDXhzux+Zjv>D11Ht*d*fB9oX z2l0n@U(e9%Y`?z?j{#nlZJQgUUQck0iiwwCBNTF-mqY94eYU$nntk~y|J2y`Ai?UI zEf9M?%0xuccYO zv>xP_ue`oF3RDe!D|tZ+3e>hRtxyiHkuWBTax+BkEyj2kN6wWstuN=!^yC`_tv
  • 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 z%0D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 z zvDUZXJHc7ocSmhBzD}%IJf&E#?I2ie%AC$Asa8CWJp10lS5aRhZv(p0HAQ08{Lnjh zdVeY9??uPSM|ANO5%<*9)g5nLo;#m#UoJg30W$^K%M+=~Tg=1D{JMt+D(Y-nlr&KZ zgDK=3+QRO?u9h~q`pyh$Y2A!hD7Nj}_#AqjF`l0*CG>{8=&1M`9Wq2$SFXLM(G0gZ zJ>2`P_xT)3w8CG3@uGtWG@Oq$>e0rnNF2#nI)OqNW%=q=IL!3I(i~wdr$$C_7Dk2_ z_`%*33VI9zbVJG*QD$bdbz(go=`!8r$}y+J9#R`8&vJ8l^Nq4XG?%=^Q}U>I>uiZ| z5SP314!{mf%QjlWJdIH4yX?=^w1j;tNWPX>g3z`=bDrJA$3yG1DAqN-Uq3n+_VlK6 zeo(66Aib}w+6g;f)IkFeHd@gu@E}FQwpK zQno;P2tH_NCCN{T)Lcuqn^MHP<5Cl2+3_cFOh!x-<$g7FBWZkxi( zxcY(M-?acEE{x_`)q5c%@am}hc_is?CiaxBbxzvwzZrHk_V#0aY33{XWaz!Q9UH7_ zGWeQT;kKd*29VTfXaR147hso7;k`e!XjsEjQ5fI+()5z(DzEMxml9atdAD09@)j4aVOwc7i%Y#GC=5IJ~h`xURiE0Y(7ec+hzPKhE zTUV^D*&K~?G$>HEBdb&U?H24RuQcczf112K2dVm(&9j-AN5(#u!RbQz;WFPK3Buol z)6`jJKJ{zWwq>zwz!oQkDFPf#F4G4CHk-eGHI>d++JQqHg_Lbajn!9j)~Kk!?!dfd z)M&Y3fv_d*;qO1COLgFSNw_n-z=ljbcfqJ1{5MbNVu)rw?Y9EOMYk31TMeJx6JpE| zCzZRWG>Wx`^Set#_w#Thvy`q;7`K>8$yJo zc1WZY`O`{beu`u!-<lJ%=~rMz_Ar~6Yod_5OonwK<0k!}X=O?%n|cVf-tK5VdbDpJQ9-IAGB}a;|~ZhtU&HjU{Wd5?KD-h zaT?v_a}>NG zt><9^*R8r@F6Z!FhloN!fO0|UP|Y*A8z0m5${@2Jo&IsiXWOFFrTkK@QL_?cK;P&eo`xV z!G8759gbfiVs38wLNQ(IZu`uVbahSCh2Z7u`%m`up2B}LqvD2h#@M+gK2Zr z64Y4r3@9pQ^9HTkR?5wJi#HB@)wWN5E34eZ4tw~0Oe|au7S1Mn)Mk#s7P1tB>Zf@4 zp|F8rYG2L*`Dz+OuHI-tCZDSxi1d}q^Ecgw&5}Jy?B+$HgN2{>@8jY@ zOh!ZF>;AOto-UD9OI*l!?L#1KO@2OoSer8IOMRE`V=Lvbtror@Zls2NUa50rd^|RD zVpgua7==4U@_lx0gkCiK4~UkoXoWfltn>2mu@8E2q_E==U`BNCj&Cr_$wB>;B%;a# zZi?O?>4Jxb6jW&aSM!8kWf<02`UraQAq0j-?F3%4x^4sjvgb8x}|MN3QQjN0UuYCcBwlr&os3vH8B z7c#Ry4I})QR#f+WL;v`)cz6(4=z*crZ4s*3{e-7%aOuzgFclVBR;(W26uk7zZF*r( zxEV<)I=v8t^XmWyk{nQsFy{Tpg-5}8izhXjx!b5o{Bxf&EL?}3;}peHk`sPbhc?**6Qjs58IjR83N3P(NSA5p{~XL zSJys#2z9N6io+B~XO}tjP7TKF2M79cQ?5|jDY`u=w(#%Ys&<1jEtw72U)RarJ-FFl zVrUXsBl}1m>^mIyH!*M9aJ5WTGnCL6<_|}6D9JFcsyu$`E3Td8{RRtgPDmI~xtG3w z@p^5}AOg+BSM)A_8uZn$W3ei_j%`o2B}qh-j({&t{M(({58tN5#J4Kl>x-f8UfB6v zm#IraqF!R0c}LwMmUdNlzwSvr>YVSOk8HfpC7+8E+@E`SvMGaBxOjMd zFM7$~p%7!&*l%)YI|mYEFeyUz#cVlPClNpW{X}f`!S1Aeh!VYlpuwHwn@oykuDqS_ zj4O{`n({=vQQ#M>1F?N-7sHknJms}DTBX3(+g3T1Be1TJiC9F_!|mBc($^oq<j}((Zk_7EN6Guhi42$2n2$~MRl{XzQ|1dqU+h*w|RJCIF#VB z=(^2Mdmy+nP=x)^y>C+DiF$6}Y(wFqK>V!DR-B$4i=cv259cGjl~@BkvxQ#gM%9bK z6z%D!^%~$4?d&(@=^%Im-X2tcy4vV9>j*=AkTv5%u98u=@bz62YM&6%LB}iAMgLJ% zOaK>;KkbpJsnyJ)R=l?hPlZ*P0HrcK>qw^7))2>D@(-j@X$sY8m~$OSnB?^LT`)Fp zHI0OL@$G4IHoWWA_MYUGDpMeBB06v#(vfb}mtT>0hBE18|yDaYd57z^eL?J_UjMVoUo--ea&bfHKvq!3+>^7*M*dgC@S$hvF_8nv=uB z-RvJQa6lUI*4|Q>j_yJP)sppX?I5}Rj5cX1{KF`}Ht3df86=)F*;>+H9>-DG_vQ`z zJP*4Eg>rjQ=?rRxTdNh{H^{VKa}LweKdP}dE-A63r5Qr|&Ka~DSc|YWdGPo2afJOB zJ{XsQ?+)Fxza&jzAu%3vYL==)xH%IZ6R(#%|$u{;wBB~mpLl?|``S6aG zmT|xVd4vo>qw%}?dU`Fb)%Em+FG;s|Ph$GTKg4>Y5*pELTP-67K->dKnh>Ibw|H+) zV!+#BTwHthTPRcvaR~}PAsrl_<3`@fc*90J^;7OscDX{pGpr~U0UWWLD(BLhs-jrY{>Ys6yD<6zP=U9Gs)zHJeQ5I znH(ZesPt-6xDg?>WHx@$h=O>@73Y!JE^XfWRBvzHn!otfM}BDpGBiFhje22871wqD z+FYlD;0xttN;o6RB{S-L!Z6~?32}PGa$Bn_h?D%D%6@H%+2f^~*AP(3!!#PHUNSU> z^~`dw`Z4JPDv}(h*4}GY2rd-qun2OH6tVR=Xb!JyU+*P$st%RTX?TCb`Nq%}c2Qf$ zgM?l(5uZ=aS=Y)muPEec>tw zO}+^axi@kR8hLmKiT&gdj`koT7l|IO)ix4Q1s}Q2-JyZF{K?B1cgZ!6xs!`YaE9*E zNcfVlf)EGrA`pLrBC;34PkoGQE%_mL0`WCaFJg&7X1vYSCBnk@sod-+3atHge)yB^kCu{ zDkOuQesi`Yb8q43*4GhexDo_W^_QlH*xS2|a%@R3{}+fLpm)el5n@&(=H#S@|L%f= zjdK$;M=mO<4o476n%(g1d%f$ky~WeB(S&K>VY|Feg31$fcGes$F)NuZ^3_2=07s?*a4IS!ZiVx`aXA=68iyKH8utR6aS25$Y5UsLL+W z>4E~Ya$PW;b9+xJsNryChw#!RFc!C*n!!|Xa4_hDRU~sFF2k?^e*N^bG5ex`U#?Py z!E(8=@0eNO)$Qzb3<)@0jTWH?f1eYyI3zua-kzqKrK#LnXKLQquH|0VdE7}Kr% zIf$^FvP}UqD)ZLN`hQvg4UWJ$BjBD#q;QoW|75%?_ByyE$;5)T-vIm5OEhq_L2v1h z>8WNMIU6x6lM&6#KB}JtGmr{e<=dDX$m%`sA3}FTq?iDajbEs~Y$E#YFa}gb?PjSM zxcL7{N_!Q``V>3NIbXV9sOTXsm+4~0b$!XXN7q}IXQB6|xF3wPHgt=Thz&q9HC#j= z*zQX>puV1VlFok^E+Z%fkrD1|fFBxsa^Dp%;r5NGRt&n}yad5NxL=1KHvj5Aw)#X( z1UrgA1_*Ex?Ac6pGlq}`C;!V54iMR)SGBZiT`i-b?}E*i5Q|68429ZIUa79Yt_v8e zpbhTtre%yLE)S1a`NcpC6q*rXF3=MS6P3k0Zg{a9r{Ie)F{A*v=$<)z#adXw>&2yvN z_5G$d!*D;A41n+XHPZVdk_cjCpqw-QcVy9m-*g}Qds5R>(w6+IcTDPs23C|{Ht$9+ zG18uytrJ;4-b8R^qm0i=^#j-(aO2g zh30pNj({2^>qayNsuMK85fS4V+G|h@QPRbj@~(=wzBM<(pr^@1p^USJmP#S-4Fs5? zn0@_XIgnUhgX_5iX2@rQ&My=!YJu5;ZtZnMZI1;3^s(0Y*?_}>4OB=hfs|*x?Tlah z6EsJNgd04DgMiyZ14-XCCkqZEvzJ zYA~8!n`;{OHJtl%&@(wmCi||qUq0{6=(88Ct=FLAhwe5`VzhS7X59{2$X^Fvo>wr1 zNh>~o{u%H|os?!lGlPzes@?Ao&xCiXXIoD)cdwLS&Ree43&L&t;K^0`do5o6wlUMcQx?baR zdAZNHbzOh)#eeslkPJ++fGlvWWjsG0W+%7T)%SFlYvTfQ52+c^P(l1$8;KC( z?4eUKCSkv;HIjy?nb`pH6l#n|Jih?k=`tIVIdrX+W_|e+A|N2*rJTGebd=jKpsHz^1CX7TjN9?Ypb3$`z z&$yutJ@orhFX+f#rA0cgHvQ#DU9O?u`I~s?)qwTfsQUJG?!(f3A8~Dsg1o)zQ9e|| zzsygzVTpoLRDLp573lB4F+L0UTpntV=or}=mq#`ojMDN@A>b288j1u9rV}+97g$zJ z11-T0kew#*7u}C{Ef_+jBdP?q(tc#7_uQ8SE%x9{V?R85P~WCNEDwx_eJ47{u)u^2 zES05lnlGuVoiO5=RQ6r#$7Y=j`!5X!u^$8E#!C#hz^sfCwtd`FX99}Huu*hib@;qt zr&AbuOHC~j{%n6&F+es@L9-G9ty+faURrG1dgaheNiCC~Usya7QW-)S$%vA6wV?u` z>w&-l`@u=dk~nDY3JagL^>BKj%+R3))=07vA)C6b0zO?t-rgygHb%X%v7b zTxgx>X@FQ(-up-hiPzy)RaA3xXbK97`fS#6e;pt1?2V>JCaRHVqV5x)hBoYq!Q4$-Ig1C6t|Hgd(Rj9;s`+vfZH-UKkw!kNjF9y`a2 zg2WLVW2Fy>3G(gC2K5191Nq%mXbxQ9TR>aj)o4#q?PQC3s?wuN+HVbWyBs&h(4T>^v0MA#8D|0*PAS)A{+kCB}_akGd+d zFz4)Itwl5QHPit?1RsQZ{An;cBzHfJ@XBEUPKuUbFCkVmQ}rF>HB}bq zS^y1;s?=T>aosi*LsG#|$3l+`%^r&W;$d9i_v&ADS~^qbHr0V&Z&Q1KkOT55X_Ub% zVrKMR%pf5?=b14Zvc9Y4`fvX>hhph3E+?MVu~?>IBIWf=UEKmOg_syF5KaIszq14W zfXm%_PQM*IF-;{+7P_Q1a}Bj8YRe;%$p@Fkb1pU-=DU6?)d|*(6lg*wgxC3L z;&Z0@5&%N@YE;xXw?(NRnK5?ur{_|}9Nxs>3u}|jd0`F^*LAb*`@FCi=_YwE4-ZfO zM0quo!f%-k3k&bt7li?N0p>Phj*c@oGwY6^+EK{g`w!mF$EVnIzA-lDZd_UAyM4L= zzj4Ofoc3#{9|~_ODhLyrJXL?xpW7Kc7UyzBP=F*tHt&r_fkPwiN73gQJ*3Mo7m>x> zool>*+u&>7h@O9C8(hnp_uY^9*KBND5>zSda-RXUuKKjKJz=Ue7Qr6b2U8e{swuQmEnGBTn` z;;e<T^OlLp@}m()uPtx?JPja! zuj1Ui5~?8(Ry_0`XdKuKI=N=#g=tZV_r7eXXEDxe2X;e}wZqQmv(uodQLdB9 z(saO3!L?qwg`ML5O9$s8h^N35IqP2JAz3y0!2i{F3XsSlzyntBQ8$^uHH8TbtHL|X zKxa*vJtzl?RG=?=E$^x?(_FynyCv6DIFXR-0PyUJ^Wqc{Y?$~D7z_RH>*k=eCr_xL zZ-Ro`RFDQ<8?T5@W>XV1Y9}hdR>Bf8d7s~9xrF{iPrA&3%2#Ddm~ln<2vqgnGHGF9 zj051{070ol+uQGHuxWvC05)eFl{p&FL7l5O`ZvNqL`1r{xx{T?7pJ9U=!{fVd+Co{?tV=vHQ?M~xwP z_v7i5s;aRi{l&E!y~S}JNS)JA;(5_U&TsRBnAmyGp!q;inYo!2RS&gv3~0TluC=cD zx;+h+TcyBKLm0L+W0aFL`D{a4?T3p^unPFle%cb)Z z$WA!Z(AOT(oU}=u1=%=G_mezmpb2Tc!<91sy&wpde<5WK+_%$(q^eztAy}$rt`${Y z<9=RPcvQ2fz3{r8{R`*MlHQd{4&F@Fk{EBUidk=QC5~j2maS^@O6qu~<%q}S7_}$T z)BZ!7GfOukJg&%1ILr?yaD!=w(0fgX-&0=ht(MI@G0lrQK?f8(lxNBhSbnV*$Dp&o+>82zDTPeLD zIMX?q9Bb-pKK05T-(D%#2F(Z*<$NMk=BB349|WvbAODiSq%(G&{$R<`u$MW=BPrQe zdilqB2VBvEravt{)FRWe%)%BLyD6j-J-ki^)e`hMQL$f3R?H{Xbe*+qhS$M10HO{7 zQeyZaI#v!`d>p2@rV1UFs+*yGS;#Rs(- z#CTvqq319x;Sr~0PVIYy)R7z&+nFe-7ifmdXs|}|TW9RgwF<&rN#Ty$eQ>!iDs@^O zrgPKP30P0aEQ)!%;A#%oIe+su?&YJQB_Iqlx6a1ksg1$*8Y%GWB0-&F1k+DoZSoS9 zK}OTxy*n!NvAWuR3;?!p$EmZ=dAE@K+?M}*^E++m5-3wOstnnji68Tu_AY|%58nQ~3r?bnc=9GaVP_^2nCGA68Er0w9(8Dx6c#9aLhg?(?cyCa$V`ADL$~!9*UT)N51t zahYz3Q8VDV%T|NWWBuiFslHf}HP1@QYrbTP06#+_!2YY9v7pE?hN+lb_P|SM8-YL( z@hHr;A1nFx<45IhQGERVv+b};#2v=?yaL;ek}pD#=p_NS>#`Fdj(EFsGMaL)R>P1B zS2aHsLI8i7&uiKlWWcJC`iJ&oJ=0|C+IFD~xtmjDe;}PvSSV2T*=pFeo=Fh|t>E-K zyD(uLWI5!K9r))S2#15ZwqVs$@y47`d}+`*4N|RP>(VHzYixw12A!;!dw`ns7ALQ(ry2E7W`6{73of0v1&SnzMJt`IrSjM2 z*rKF_PSrOs&=7q*ALuRtb-s=u>a z;T1FtqSMoigj!mPOs)b$-07U7^llJxcV+RbH7(7B zZ>Y3BO898FIE7v^Ot~EHR+cuxlJx|D2fJa;*}X$n1ojv{KK$(nx^w!$FEf6H9W8)E z0)Yy}fX#R4z3FS*mIBRqe!=Ymfv`U2FS%Ojl-$PYuBF~w&|UmDUH|X2zb<5hCh1M# z*WqEFQcL%n7UdoKM0W-DO7EBHezwpTzrlUgW(gF;s-VfX6L8SAnvi?Zzs!@K;>lNQ zNH3kk1Hu;OPC)$Ra-9^hrd>Bh71Opp7_bL6XReP4u#8!e#JmH<0Gu;yoR7erbEItR z%cu3Rzt}^%jxp8TXfYLNEdbr09!q!?%9D;?QEktN)LB*uI;)`XpKf^F7uA3!-PU#o z$lpiMApx;nCCE@VKD|6+*H+4;=-ns?b+K_Ne#rc0r5fwY7fj3HVL%Wm@yxz=Ii!$SU*0*stnfOY2nj!G;u@f95I5!JQy_|Xi)6=JsnwxRmm!Jt4+rK3H z8}xm=T`ua7Wa^VS&c1dTa0-d)GS6!YoZsnNdLMha=DsVw5?835?6BTvp+Oq8-AS{E z0OScWbJ2+K%DT3Qj@9s2HF;M4n8E5AK3fNe@OxpN;D^*pPCo%_?)IUbhL$8x!zVVp zA|95L{yEK$XL5X7ohG7vUPp{_6|`=Qif6HmlP!%bYDr{4izE3F2)UW=%7+n#nY%^W zXlfOUAX11a!4L_aT`-6mIdXH=n@*>T#LxFP$s!yfgj7{+zy@s-Sbw)=PW}c2F~|>&epPW&U_0$S)T#LH8U`46Yxw?zf| zGp#zNi$Q9jBdXMOd#(&)EZ}6xc}J;v|1n@=0GIQoibEIWEiPs)lBgIrV%K7Jd$cL7 z|Bp(e`lZn2a>_}c^+hycpvw-?vX03)1?;-MY_RS6s1B@LXD1FI?JB*f!?b=&BGQyU zJP;*TOp_3wekMwQA4kh~%?uE)f~VvZ+6t4tGWz#bQcqtg%1zOACx8WPn{SJ))FK|~&a zeJ-rf4aGxq3Q>ess>#2^1oU=`Ws9yOw|QKENHU7zI^FG^og=)M6S_@Z45eVSoQh3a zOkp7@&QjX7_vh>bCtEU%_1*LpLw+iF-?`VA{lB*V$^-hiua5}<&>9goN7lXC_=;*j zKnZyGNy-0;f*U%B{woeXN`(cOP%}(bjQ}Dy+K@ScajhIEX*+rBG; z^#lZmBsei(oG5;KWd(E$%J0-j(2Gpk=K1Gc>Z(8>;$`^ME%a{4BlFHPeFKBXwS54A zfq$Q*@YYVbbr4WEM#7l}eA?CY;`B7;=0hs%XxNyQkG1Qkfw?xkYgI|m!Ul?7mhA5t z!62GsxU7j=?5fw!;I7vC!H#O@)XM=n-~qRkp;gkgwcqgNZY6yIEP^Lp$FrXd$MTL_ zm#T6*HfADp9x&8F8Iyj>axlF!o0Q21c<#)}K&Jmuy$&Q-a!6Q4^{7!?CIEx4C;?CG zvsOR|1le{Gt%47cra{3_;vipY*uYl|lI%y9_``aknpsh~mLCrpH-`XyAKIh)|rSUtW ze+RqhSHDpiad2mKOrvW{rnE zrd~4C7u+UZZnG$PIT*L)Tt=#jbxNKlC^r;s$cK8*IeH5Mnkiw5PlTl?;xA{rcyv+qD_YpG0Vp!eu$Fpd|aTrooP=+1#ckw8Wmz;C#%- z_wek;;5lvczLFH0n%gU8;#vU-ZoOm(s!EYPT8~{z578Q*ETMr6&8XphK-2*W)2ioPO{#-5`lo)zvY^)VT>b<@a zGU4zzi|3Vs#Y)TxRZLylHQj!4{&ajL$soPtlU%7kXv7idv&WrMJfgl&mX{Ghr`16b z9iQCqc837j(q<=+pkQI|<4W_SuyFtA!R+INKNKF7w<0U-uuRDsjlo@n%h=EHML~hL z3VMS{EF;t@%$3VCFf$|Qr-SY)+7CAZ>a^lwaA73z@bH0V6WMMAoTkALF5Dwq+n#=^ zB;#nqk~;y7XtME-Mf>4i*_C|hf47F%Kh@FUab1uRg7^wd)$y_r_s%>agAvcYkAEgp z`!PYuTN@bgRKjnjABvin1qDPJ1*D$!VeC3@K(r0dEs`;JxCeMijRM0BYnqrh-N7l~HQJK%78P6cxyNjCm1vOA8n#EqLd6BM`KUE|Cc~qMls1X=RG4|UKFVK8?etNX% zS)XF!LwDNX>G*VLh^O9DKteNj7+G$9 z&Z&8qZOAfFG`_zLV!F(4W)46JR8VAKFyGq<0ua!CA~P>FIYVf&WvWI&g~jzg=ydvq za{mVwDLT@QFP3pN?_#PIyl-DjoqWIdd)YXyD{|1fYh^giuY3X(z;ijd&jLgr8>f4r z83S$!@_cy5D6b2k`#9J1WXMXl@w*Fmig-5*kUmckXYdsw~ofm28#ATc!+9U^d=`NTZW zG|{*##AWVIrG#gy`LBm>MTu&F*`_WB_bQX&mkw{CIJN5c=`MM1(qID2)ioR@9tO+6 zv~6u|4Q9?vUY88?!ZLaWAz4kq)O}#Bkd%@LRW=3TU%^i`=05;b^2Pln0Gl51u)2G2 zg3&55TM~}c*?ipbrk4qG;w^9#$pn&1HS;7?EBK!lU@cceeWI~G_1(Kux3+v%UL>!n z>sqGEnAyaRH$#Svd&h3z5AG7Kq|HszqSnPSz%j2APf()pq;v8JQ%dJ2u5M;Y{Six_ zki9mtr4#jc9_2ZF4LJRv@)q%}aO>X~>4jDvMhu=wLsLQueWj@;DOK}q?5uMa3>naL zjPRnI;V?n{Im`_?9Q;asY1I7ZMG*SrWHU42|(N0mn>vu`C3^K!}T5{ zM)bo}iXziy9HNF8IwjMu?R6dRmx%@_u(#^2=w!+3lHsn1LTQDx?iLE#OY#3Re0e40 zu3uxe?-~ot2=Sc(YlV^h=ex7h@g5c@g)2$Y4Us|5J0)a))o<@K)RCA>RkWEWQMYd=;-;e6 z(Ix^4cqn^>At8qmR64)XuE{u zBg_#hrGQB%&hW(Qjrdn^-@9|;ss>2Lp{Iq5q^SjazCpkXUtyAY#O@+{tp4yDCCt&4 z57N&;cgX>X_F^^Lc;$R6s6xjR&JKEQ;bM1dST=+1M$!KHk z2<8@Inh4R@`)?T5RLGpB^m1m(gIXN`tv!Em9WICn(gLfTPHU;tVu5S#Y_}3G2S^>`Ymj@{66*%x~K4OSx;+ERuYZhEtsrO=Fq{au^wMz_> z(@Bcz@5L4LOskvt$G>$y-_Mg~=94nvA@BXJuTTV206w2z3s#-XHDLNS5P0dwf zK(RwoPeZG6kvp0Zto` zFBsaKHtNb#%7s(EcwiUGmQZoX_#*6zZ2WD=je_zi_m^ZbKEq%|%N;4$BguzbvhGsT zZB?G0V9(Y|?C5ChG?M{bQQtr&JA3nAVl@4KYS&O;9FUEaWrRGvtzkLOK?uXy3va`v z9u@|Z?Z?$qU4!R8N_STuToOSoWm2;8VoU532VjLOFVllPb0w-HbWhXz-(iPYmvT>2 z-4Gl&siU8F1Yj3{Xxbs0v_F43_)e3-Q0O){G054E5qtw2+yEhzb)*ogKUjKEdgb%OfFRD z?dv0-@T{$fVdw&AA1G!)s-cg&t(_&56nUR1K{`1Pzef)b{RebuPDq*s)J>H6!+oIl zKz95CMoBQjhL5ibGA*R_3bc!;ldP-Itni6aTh2D2T9u=utB_PE<>81cnJ$F7=eahD_+0C*jf#{Nos5_p|}$@Gy> z0uPz=W%kyN!c33eF5j0Y-&BVYAkuZ>}*1*T(qZKY>^&!&LbU3|O~ zkTTKIX35Trb4Tds;qDxIG6358qmIl1!WU=|5$`l=@o(|_^6h~s;WLwqr3W9)yPVwX z>Z{FCxH4Db<$VRUUY6^}2+aHWik4YCdVyqFX8ivb3XoG80!g4FE)kRGNWArCjGH@Q zf1h@te)gOmLIQ*S*abbgRUFZvr?M6s8_?CN^9rJwEcAoKafAI_e5w0;&+eA{n0JLw zJFzFcjY%y*ilkGO!Pb_=>F7)5qmhL;SD*76Ek_zUD|77;PRMyWgK8^QawRbC!Mr1$ zRSge5gXt{?g^b`zWkG)@l!VUBez`7|AgA_!awEyws%x3zHIxV_j z-}BM)6Iz^`DBGuFLKlAAaKNs{StEsEV2I$6mM-BfzIOxzO#nRrP+05hks;48GTByN zx3-BL%m|j+y+v+e{bhM0}YTeoe$Wped7Xk71b~wuG_o>2$3KQ@-$Xm1ooF-pwz7I{TW*_-A zGQc5bMd)F}D1Mii=L=;k;921UUcdTki+I#9cmvsuA}7EAYEmbq3Trc@A*82^`QCnL zu{+O&I>C0hH9H8~EK(pr;?29fnmuiteV3E-Na=8T{12w@ zcm&S@DMm*v@es4H(0iwGtJr43#XItbJVB{_kh7J$s7jxU@Nz5>1wcZM?UM>+%mkPr zJtFJe{Bj?LIk|~JYXf2nU@$s6lVWUA?j!Lil;XR-mF77#dZ?eimf7|<-U6F4kk4qc zJ&-hka0dhSXw_47H*bvg-Znt*Tfx=km^>~TT}aC})E-*hBtyVdd_`VYvpU8#+#FV3 zse^}cyLsG=up+yWZ+S}eLGQ$`1DFhfkY1Q1@9uSZ`Fq;uWv-V8^TkSl$;N33 z+elO0)O4pO&BvHl^k_6ZbLly(E2png@eHtE+(ttqfW3Q|pPBA4qu;|`zcVgPiT5lx zbNrQQC;PNxz0ql5g65>x>U3bx`=Ywu5d$s7Jhp>9k*@qWEqy1>;^eHlS--aM4p*QS z7oXD*NRx&}nGq?J!@BPE60tqJG3^8`Y&wj|hK%PKh8UBC6Ca z)9pC(CuUB3HZJnoohiH22}iQes*Ng^C|%G!@Q=S|U;oW&Qp7+Xe{9j=!O5Jve$`p6fZr8`;&<1Q z*i1bt!yT$B9=SQ|q|hiSQISRov@iNZ$38!lIj#nA8Y8XeqsO=L?_O0a>Um*FR#^DR z_Euv-y+$(GE9Gw`oT{uzcJDRUAZl{1f8$NZulcj~2pq2xSxF0zn=g-UH|!?qI37zz zya@--qn5@a$xwOvRFIX%5GN?9b^l`z8Z_ zmN$M4nJoTUFAZhd>G$Tteu^j>L5DqXgoKzi%@S7tQ>Qc4sx#9j7-{T*o_dmPS5@EXieR&32MX=Vf zeXAi0t9j5%>YesKOsRXp+*0Q^jXxGT*$Ay9RnHR?c$zYdSqCxAg@t#uJjNaBMJ}C- zPfKgNf(GA~4u*Af@RO5&J3blAZizDSw`tq^(i$@Dz1^y3QEmw>G+?fhC+GO{L-G`$ zs505I;Uq0onb1`(Ui35=qnLfG(P4B#b%XKEVWLz0M%C1$5js4e{7AWSUHvaDFlXt` zU$LkP`#&N{ShTMxlJ6gc?f~^g#~A-ew>W>srPr12X!_{JD`iXnU-uYHz2u`J%yP}Y z&hvoQ>jtT7PW4#SrW0Xn$wAR3sL1 ze>Ad=LmwoLV}wP(5c>h^p+V)RgpM^k`hbAInrRA8(f~&{XJ~Pt*efqrwBWV;ZifRe z-q){@l4kw$oGHS%^xWd(#BpjO6H#NM@lRJW3*MU?+}1<{^;P_PAIY?JaFJih^ePla zPiMDa-Bsuto3H$IY47fhmC;*_*zsCfX+t<0n%$aBqR!7JQOH-Kq{FY|S2NI6$T8M& z8PN)Pm8E(up7jnX#{OXzPAZK#mOBTh<%?b|*oBM!^jgc(C!`S6uq3)kl7Tms_qQh> z#gpG8Wx51)1hDC(`es&mxS%-}@&rb~hi7J4yef-=Mc}^08b0hn2S0|3Yl|&yp1276 za9j<8&b5wzTT6x2Zy5(OQC;tixluS~a#?@ue7PZY`^^4EuKLq1^Qo)Pf7Qk6tLSZ! z8X6B=%PxgKuMJC_YZ`l)TCMCIt_`fPtC5n0gaiVQy!9H)i;e+rCVu!3_XQJQ;5HhD zgv52QJ>8OZcHVlozFrq=Sn|~3)6l9;@fXCX;L+q&Y<_YdLxmSCii{;qzLas2qH&?2 z{IISKbJwh_kBGEzhw&F zxZ1oJZF@gYpjfSdKX4Ov;z>7_%x%_Zx0ePA!0c#b2-c1DvHIVijK$Bk#gT8zP&v1G z7G@8?X#*xkb`DfWhwG(nx7Jp77JdyOU+s3Z_5SACpdf@gtnAf@@Nj)#elcZ-czB7Q z^;=CI!m$7Ii3zQ@!#7_k6L)rgb`hrDoAvec+bMVuK#KrQ3VPVVpg`IJ%~gxwvc}vu z->Km(n%#yWqR(4c07=pN+7JXb(}Ap7g(jkWg7?$J!|LpGrR(&AZB_a)rFeS84ZKoP=OhEz8ksA*t6RL%8gzbPi5RZzLL4 z-J{AZ5oJ=@ouApL$tip+)up>C)5cc3R6J7I&p3+ULbc9+1m5U6#D@GX-0ZWUWT?K%ax>0u!i_ zuP5Hh(TEpvk6f#ZO-=1Q3Uc6xb{Ve>^l2<{7?)`{v@X*{Q`2pZOG?5J&dkB*V|-xE zn>sY+bp?F=uw0I}m$1W(urV`T>K&&pY9(N&mrKu0Bc4_MW z*4G!?$QZL1tL>?M>QL1;5hEi*7fTl`BO{|C$X6a|@hLouez2;l(k?CbQ0f#bQBDTpobV)2l$syM4LUP0zNVdJ|Jv> z)t40ewqDt`T48E=GfbBQOPzG?Xe#}pX33Rqc=$^9r$~i%x7MIIWo9%WQM7fUUS(&u zRW}`w!4mrB&t~xu!3-yNO9x3F$f&c8KpJ_QXOIYi1}*@0s3FZ~IYnYfSM5J7Xlo5I zdV8s0(ivZo7*s8fe~%MGih<#NmTLXcACC~Hg$F~(q zygHXbiOX>C^1ppCM>rE~;C#*lVuhLGP9azflzB-|r8w;VRwHJ6X2sr-V!D8!rQaOG zL69%-@+c01HS)m+{mD1~>H*LnnYnq)N)^eq-7@=*cjR#%>FeKOL$$kKjbQ%)ve_w3UW|Hp1Dk zprbA0x{a>!2$%6tFy6$I%Y{8$==44o^l4RlS9y}y%sd6J+_ixX=c?fs-yc4>p8a4^ zV(X+7JM=S8YG2JxXUamqb0F4c%_50d$&LdAa#GolG5e6#uahZjN%zg`_g3YDAFLdS zZSSU-Tu~=Q2J(O-6$GNB&JKZq|1+8WBgQiq(!+E&c?AEF;073L!Ig*!Vt+HFT;B^N zF}WE+0Uig_kPZ2S1BPUFK!EX7D10+GY#G8tA%$xY8BM)~Ynr~B*N=+20&=6W@!`!f zu`JOWR&Z@@LRwdHyIh{fz1A$eLF>6ugVwix@Oj2njub(!n6$g*P7@l(Y*HF z$?uwrKaat47}DBK`fT4FE!Z*ec3sr0j5ax$m!+QNfn(oHVX6O;y?4L;OwH=<#l>Bj z+%m`Ml~4s{8a`St^XD_QqKGl5tf641#YBkjf2D=ZAF{{(1bBp4IqRY@MmmIH!=tdC zZh;$$V<%ACG4ik+j{ubadnvLqGMO=d>wl8}zxp!SR=+2!e*13*#Du2w69UC!R#OUv;F+p zR*9oCn{(lh9Al4X1Eks>1n%Mk3ZDIAXYu#TUdxVyu{flfmb`a<_QDb8Q$uC$dR>XR z?X3_9Nl_i)Wo6q{Vm2tR)w3`0;DahiWI6_tu zJqL#O^QdDPOpIrnypLTTJjA*XT6PT`UC(XI*C?0+zeRevjJaN#8+Y;N!J=wty@YBp- z{oTIURsL6{Y_X=ia(ZPdhFrEdH>>c|5FpmLW~om@h>lg#;jBKj|Az1v-OscYp`pen zMm*PZxAs;MZUWIU8Jko^Y}i2v3SlCPMUHY9E9h6fe=mLJZ2Zmt)Seqi5%y>Z=8u0H zbd^R;LBH?0IhI6u#GE+I~x2tNh5ig4b1k!c@8ixFws{!%HVj z*1=sqe+p8ga^Q>YK%XFPaQ;)3-`BW{GP0R4{0+?XJpQXoeHta!wJj~8HcOpK;_-25 zPXv*!&MYN~Bwl=Nvs`2A&sa)2)b0&72T=i#4z+Lxi<&`<2I6!t_O6R9MnmFdU!~Di zx7@atQ@aQ6uiQvS%qCf{b1H&LWNeY+uMlElQR{ zj*!=WL%rgvo_6hz^JmP2<7K14YvkC>FD-tlQ@yq=)qitT)ZELtre;l^y(&u`&AlJ4 z$mqr@avpl|VxyxgL)rbelG96AV4y1)m)$x@tZhf^fzsfRwxlfn6%E{CI+itxz zLG99V{Yvt2u50uC{*F=v_&;-*SoI2;5Diz$ZT;B{qT~*mm-E`&5!dtlYPf;Z@Q#j2 zvfPP`-j)(hOAzPYMNe~Bg4J3&Ki12;1X(Co?5L6K1qXUi7$+%l?ZvuM(ol5-@0hZj z;%D#Ap>%Mz=XS?LMC|h-){QwbhlE_66Xta<%FowPe|uW=a&v=iz=oB&)x~{h&Zbt- zaW1pDm;GM%a{e=?`?xBs;GG^vVPY_DF~McD$MG@-x$g7I+Jrv3E*PB{7*^Nz<=p{A z9~RZc%GiE1+gu{9T5mmKoq9n@DM8aVHE+Av!}P5BGe_kFGcvt4q%`{+AhhGgXP+k= zq$3zg@12?eP4n=HV7L3G_ai#b>PH{^Jp{60AvKW!50e~9iC(RWT93f(@Iro$uQvG! zS(u(1)MCem+!)1;sVpyEofvn$bomNoU3fM1!|&X(uQthYG4~5i`yD{nZYwCa%QCtY z-;~S1I#pBuzS3=7ND2*Q*sS6=p4kzQX2|#Bx4)J1dF7pB6LaaNF0Pisf~+YUY2Bx% z&7@6&Npu0__GWhro_Z53Wvr$N)}W5xu-SML58mv##ynG!j;7m~ zCH}Lguy?#GsH8eHJgY#C>WkAY^)bb3bNUza!E>2P6QP>CbvJg3rFvYuhOw^|XIv&t zn=iyka934n5Wd%sOO%ojF_GU4%y;(Bb#xK`Y1_Y<w?1af7a|4e&-TWYkXk@3%krQKS8D?a{L z3!p3*EK#_c+TGg9MW5Chx255GeaEyH(Khwdu#eRN`DMBwcqmP=sAH0^2YxI~E%8}j zng2W@p%^d4$}hr+1%`xzV5ci)Ctq@zO+845>7f*>-2En%GP@lqtIKp_)0|IzX@n0H z(I+%KR~_{_FHfUyuhbE+d*7EaS_jXgS|2SsSKY6{ql+BLh(sFvtV^GtpV@Ws-Vmx(}6qEN)I}f&zR5}_= zk1IEGoCL9}Omnigt0^IA&rC|m@74EP{Tq+%D910cFL~o_y}i|^lF0j23#9Smf?*I**rAPvu}LW$Al7|Z8@v23tpflpww^F}1<*%C@Sd>(m(|Xz^4oz2G^k<{3%tNo(pGlxIQ;D5;E|*fTqb41WDO-x=qM9|G2YLt zz4!jkBkNRCg$ACX)qxNj+jfFECrwQ4g|MKVeWy>-y0hKo$Zur`7>ef~0^e zTUPpzdrCnN%AFt*U0YiY7aQFli*K_HaJuB zGNH~SI_$CVU*SkDD6Uef|JKn`?BFWEgWwjwf^u6vIV=;UObf=9a-R~jzQLDGRBbE2 zInzkS)5L`{rPoh?ve|k>ytb|i2dr5(Y+$jzcv^&=kwx;v7WxOt`lp<>(>**_jRR9d zeJhjPPf2$XxWjyMn@oKOv_V~NhYVsjtd6ewGpMc1N~8bSlTXtAWn4VNd-+ACPldnc zR7kn-3INsq;{F0vKD@|Z+o*1H6p81W+qAp5Kjc^@J5XePzT9Sx3*?3_MdT5N^%3wp z$i^8NmnUT-njV@z6_1muaJ&sP$NzDP4%QA#i!5>X!Pk*TWyfiQ)`|63SFbNy9xe}l zLQ6;YC|oB26Yu*fCyV<)9CH@Es@2Qj&`@tsPS|sw9Y1C;un;h<&o3xwTdpEVtj0)2 zf#{guGf&SFKi{g$91+~iZU-DtQX&BpPQP-48dxomF99w?5%_hE_v3^#P5|oY2yRka zAmirON6><>XD2K?{3rXmxeRBBhL%Ar6C+(!`Cm-Ri#nEh&EYw7YF?1tv)x_IuoBOf zSWH+kI5*cgttIiHI3UR~HxiQ;gaR?&LC4qEx4{{l|EDMHQ8izlPYt6Vck@i9((6sC zww_hPafd~EmwGvees!r)bShmtFS72R+ho7{lhmSFZf`S6S z)g`qc-?BO4V!f`rZnp6?Db}oZlb=8R&Vk4i;$ERqH;Cmk&EbCQFz{fLp3?$&+v-y# z@dK%RjEBxHzbDEFb`@=^vwr;8%qZvWgvjNY%D(={9VvxkK!gPy!ATwg6m5FB6Xww- z((1QaDX#9qoMh8BrO-f^?>;??;8D^93vG8*hoXbt9Bz@{|w9yCX7p) zJ1+dOJ?&^z%qQpS8Bg@~If)=hY^NdMTzD)pa;216#Q{&HyLSh(z)(&oS>^BrsjMsx z@e8Xi>>k-c^4kb8N@~&f3;!AUHoc(16@(&&u?dw3dZADCM#ujwk4{%3dp)`6A2al9 z%NF~^5Vgfa@0-=o%jWaQH%og533?4K_+hQrq!;7|Q8tCr_NTY9X#j+B9+N!o($Nft z84dV+1k&J4-vET_>oZ-EOJjzBcGTd6p5c$nSPY!3bu$%i42w{E2cZj(O-1(L!G zHjhnnG~2Z?-o*8&^TUzn=Fwx{&L6sK^7N@g7mA|7=x*9`Xms=hYOH>rasosa zy658e*aV(CCtOuUhYqSZf()V*jOG1f0ufrmM~^yDav^8wtG|Csn3_I6EXf9!m}NT5 z_N~d;o2lQ#j2Qa%tvwhn%&H4J6h?YuAG`DUl3%L16+vDp!TG{g;BMmDTNM-*t{jT> z@E^Lg(6bsnseY2J$b+XbR;QVdk=_U1j-w|f2;Yl+-lpIir2zT^Y?^X6;o47bF0X)vD`CN}WfL`$5 z%F!@sa$KY2>|29?^q!2u+oXxbj^-O%l6DULZZ4SXlD;@5h>xla^=csc|p7vF-#^08SZ>( z@FilX%nn3KsdpppXbhrRVhvNTLivFsj6pJrH$?K}HP2Ps|tViYL ze3&WAbW(nHe-!L^4LS(wx6L47Z}$6^eMDvYV*5aS+?GjMCo|~5W?Bvv09N*2Y$pGw zXV@Azq~^zYB+2RIF0}IMp1PH#4Z^`AkQ|QGGVKtzpNgV!4)*uc3h;$PPVO+6KX2s> ziK_)RW`?ruqLG_8X;^6O6a?*%ss<(=W8{y4=Cs>q-{Rs>><}21ocd8VxuizF-njt2 zcI!D;c6NI3;FXkWqJA6xEfko@#rE=Evh6|^=1|f24{hqJg@9>gJ~cHWV%xcPVqXo$ z*pzo!3vqL!_(1{u^x8(VRU)da_(1!4UiNgKt52L!ih5{c29O5LLnm=PRaSxWj-23D zL{l~P>peX)PbZWER_5wXD(Jl&NsQa$!=d^&`KlCalBPy{_Ux(w=lbTePTOOEhz15k z-=EnM@Tbil)>uE9gZvCx7B#WwRGD!(cKo>5W{!c{O&-@;>kXd?Vd+Uq0lR;FE-n)S zqK(tB*|PP-#Y+tz$m-+X0k4r!gUGBxYvu&o-oB|@%85^$QBpx5 zxTbmhdLSQ5SXD4;)6fgtOR=e0^n01D>(Q)hWGofQNbX^$NF2O}>b!}F-J6sd+cn>4 zYV>*q-jX1NT{YljY5sYUf%Z6ftf!=nVDBmEGx!Jb+FDIu3ktKh3;NJN*5*r}_o9_l zb?c)!F!(FXyDqiP&E75U?tZeXp)B1=8IP1X-l8!1HTCi8c~twxrc7t@Rxj~D;;F0TwNZ6+|E_9-z&{85F^sA% zsc*S<F33ZFuoxeQ#gJIm!nVw zzL(+uY5@$qacWycWhj3TSZK8huK3)mfpvKy63I(nep>n!?bdR!AuRlqEj`0|V*Rz+ zd>4&)Bd9)b3YVu)0UvckM_xx;URV zU6Z=}R%Gu&lsc+Sl@wqd{y8Qllsk5G_g>z!gf9;NXBt1>*#%RIfR9B$kB?_TjKtD5 zlQuJ`+A{U~M5HrEX?OEDkQh$KxL2M?4*BiR3&QP=^c#023}dcv^{KGiw`3JRJKF#~ zN_S`usr1hSF`FH=Ngf=n)tL;Q2IYUVk*HFTZ7Tn;R!x zpZ<99P(P?r)Qz>PY4e+iB=^gigz29gDjF3j>xO;eMVS{d_a^+9WQeZtMf!G|%llP- zrdcf(iivmXQq7=M83i7YE$}^<6fpEHC#;M3q3rR5GO#Jwd=xO31DN_0HekEdA7=i& zOuKR8KhOet?z1FcSu$p_yIlOO)AbC>S%A0TNUbkVo`lrT_TZ@7M!vJ2bB%d$%OT54 zZ8|o2d`jJ(hsX`0;^}U7<(%dg*$1QvbK=f)q~PN(MEM;S zbsxh&qk`MwZFfqX!{n3YA3t&rjh}3#o&E14@)qc@#LpPQjpOIh?It_|J9b>y=f5bo zy2ABeXd~W@ElrJUsKz@Y^X5r6# z&;#s@+&V)gd`{mPlN#xvt#U1**KS4lEAGdUf%wCcLz_ru*|*P)Ca1n(Qk}8wG~SLp z_-sE#%OKwAfFe06#{iozzuuB%H0!!~JmF@eDdZL{pN0q9Z#FsGRm8d$1>d=|84l%D z?}$g3>8ILEo5yV*elGQLP-P4G&u(f;o1h6xLO&{XyC`*!H;%>w^M8z_zT!e?jMN)B zJ3H%>a15v@k6MXH*T*{HQQ)e8Zo{>j4n9m(eCME7@9++tG=Tf|EI+ycUEk2q$;}5# z!@;wVUOg`#CtiAdW3|Y4rEeS!T9Ig?ppAmHEZLQ3V?!7AMRor}HjXel$rDSlJ2rsA zq7|l*1U;dT?eN8%p8)ljT65vJSBEQwPr(^r&=cX}Rcgv=NGnzX)IdBVk1QoA&94~j zBy=3Onux(+jL8#q!*#Wq1R!R1Ygx`JHA)GYS~Ao+`>}62?5)Jf(_5oX<#$e<{}t+A z2JsMcK}Qe(npWJd!m&5=KmGd8Tin#U^Q_d;@!OT@33V zWM23Pi-X#%xt^zos_a-aQq-!dm>UnWv$c|PI+oy=vB$yuBB#AoonY}|uS;_AEs0ZS z&hUP6o-6W9{YL0Mpc{1c1TiSFD4)&qynOFZ*~7TiC;{91&{ecr#N1Hqjc$G~@leVc z7bL@6KqW`(Tn~J)2W$y=#;hkdA|ggY?JgJ6aS>oj_+{f>+@J(y6BtUYGscNG5n}oF zlqqk;Wed*llNp=(1ypcRVp`_iurk(Bj$aF9Vt)2UVy`-1s$C0BdEZsY=7xulL3z1L zmd2u>h}+x*!>LX;iQpQ}G*rOhb^LL;z-Zj780=+W?VJD6S#1B_nCo?JJw45qEgH}* zAz16*ue8XnhqOg9K8=M}F}2P|v|CtT|l(DB^d|9#E(XXyScL3ky!XHSGrLYpF@hhU>-n*vzSg|C_n-(=FI zPWh`q=peamL)W+KImVxr2@5ODIn8H7P~DegUgT|C?%I#51sP05l^Yh;S?~ zxb9iMvYreq)1=GV^9LhPFaGtp-QcbD=)O-vK@NcOkMqAA7Dt17+}(YTc*R9%sX(Lz zn*pV7RXQ2)jX*Ehyf*6Q4!RHZEE&ZflT6Lb{pbDEVl5l%c(V z0W;NJybpdPb1j`_>=!)Z>_zi=f)(T(-W2vPq_&f?6s1?+ntAg*h4>D(?5BbT5+A0{ zTOMyyK;_}%1V-9#vzB$CT=!mwYjyo$uEb0aC)wWYEfq_T=(zTMVz_r)Ns*SW;^W7f zxhN*Qzcc^7zhk$m+j^sYf5~%E)PW<0IooHJggnUtD@IEwSku#T@UWW5>`Y#jDkDu~ z89P8I5T5>wjaMHypD-w9wV#CvRt0(Twp3A0ULR=5xAfC zfF^$D`11BDsfO?6!W=Gps<|vfP0A3^+Tmd->79I=h=lRcQ%0f{^y4(<1M( zGfn-xMFTf?k?VWJwIQTki2lly4@SvXz<`KdrYzH z%x_lqD<0wF#F^y7)gjI%wcXtvXi&h#!1imkW2uz{b5rOg820RSWv0>2D zWoE7i*P`FvF)b9N{)mkW>Pi@8l-W-q#IvTSqTegnM4Wqjxplxz#@JwD3uZ(XZ~8Sm;i%8L`4mBlWI|FLeK+QepfFV z<;`cH1A>xA-~fd3;bZUDkX2Dk%-$O`1_pI$PG|eHO3h9eoCes#1_XWLEE+&2<@ftEupy?92!QBEH3e)4NDJR1TKcEu4pZ_)C5Rq)U;g}1 zMIeA{$TsNVc-(fiL6-cqk#`D}XgEZjeDlyXY6eh7xVd+Q^0tl0vu3>rfG_N~Y{BjX zPj^jby0P)W9X*P$`j$lc<2t%WaRVaWDZ|5<+g>z3!%5IGswVe(vr#w^H~Z6#8`zcc z%{K$J$146R1O#E>h-;hUMu)2;h}1MG!I6<$sLI4Uo9HMch9LKlQU>oVAV9!_%5pDrqoi?qQt?Ur$tN4d=)Qzc9sWS zNKXUiq{89UqHeN_t1CMQDMg}9uBoU`%Z^pM7M3HoxY(7kJ#d-M*Ygqru#!uPf%D7CU}R*x*_gTez=48*`=kj<<$iD^Y4y+& ztBX7(=`YOZ<;OXR4J`_}jV2WLM+`1Y2+%gb%UtUG>cGyarVuEKbkhAh(@ z&wZOwUxg!1CQaPCZ-Ju$@aRsE)^vMTRr*lmT`#uHGRxr+_Qte2RbB!cUtY!nQ_U@iXX?^PD0ZMpY0 z;|cjd{85v=T8{)Evhn=F5Ee2ZVzdoymhgfRUtDzdK{irzg4tmDw$no`ZZ-A@LB- z)^K@NOcqVO8(MF)RMKM#mjvR>F%cqPE%H%HRJlOiq(*Ik+Bj7fcWmR$#=BO3%pr6D zFn2b=k=d|1dzbMc=zg9nvUyJOdmgCeMA|j!O*PQ0aV;aHixXsmH&GWfX@)pXZopR* zf6othoc=*cA8{({eU>S&%+8E|ci$W8CbonWof#P^l@eF@yMphlq=1fuKJxUUt{3-) z8Xdm=&bf+et<9yR;u2{%o!22$cv^VvlQSgdoZ#_&dYQK-wNqK>rZ*QYk3hIrr_1BVh%QJ2Q^&gGXoqG0BgA^qZrPhMwm$!&RPXnt- z&D+i9S$r3d%00=n_N=FIbvR?B6Q}aKZ%iAsdn2Exgrmu|aHRF331Z&qV$|gEv`GE` zec&~&#*$JPy+QJ(nR>NpTl{tqyl8-tmeVkFd!m3qU}-<1rD5DS6C190Q-ZtlA)9dQ zy3*b+%*gw+)_!{pBhz1yh5%8w^$Okk3>2;YEoS;6WQi;9LBarK2?5j`PNuk#DE7T} zkjv<(0Q-7isGjO=v#*h z94u0|XNgPCT3e8VYaxM@j{?o~!XXoHWHU3y5Y^sG;^}jOT`k+Kqp*=`C#7R=u#8<}$U4W=FJ=G0!5sypM?hj-B6& z=y3s#O^X~`ZV5=0P8{}1^lT^EUT>8;$xwTrb(MI37Q+*xb(`NC_rr9s{u|?U^ z;-&m+%5-pjKm*i@RV!)DkCM^y6Jpi}v-XU1qbuA-6ZAyBOB8 zeE(!uXEvO+Ku1rvtnhGINhVGoIs$|WH&3gMa)BIJa~_j|8eB7xX0J9wa%qOqGX%rz z+q2^afjpR+B~ny_l}URdCv`u4@M8u*lSmt&p7> zwLrQ{Nks@pQSBTPpAY&4tm+2@-i` zkgxBBFd`4S$#r%0hCnPH<@Pihc3w!`RqlisuD z_3&0$!#IShXNPo^h{CaT{qvVP4Y9bbQ`GVdei2`2!t3v+7i)G+Y0COO=`t|l>ApC+ z`UdLc)Gl)YShdSJ7G`_{6%PgUOh-+&oKCL7L0&Aic`$Z$anf&OPin{12fQfD>r`Kj zAM`>$6dvuXZR4)K+wm)Q)=}L*egroja*mNgI8gr>F|Fl!ln2)OSo!ZGQU1zXj=IX! zzt9qj{`bQ8i~sowr98g&=l|n3_H(t^o=Kj7KOxJx)3dxf1cE&+7v;Ze=|w3Y(Jbne z5B?27ln*&c0u4UwjzQo@AXMBV(BmU$Hw`mB{^yJT^K$;5?i{w*>)f4c84s^eHrEmD L<60z5yWsx?ZXCTT From a75ede3727812f6125f1c3f6c7db36a2203edd19 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Sat, 18 Dec 2021 14:06:08 +1100 Subject: [PATCH 0581/1681] Added Krackhardt Kite example --- .../betweenness/assets/betweenness.py | 50 ++++++++------- .../tutorials/betweenness/betweenness.rst | 60 ++++++++++-------- .../betweenness/figures/betweenness.png | Bin 371944 -> 167428 bytes 3 files changed, 62 insertions(+), 48 deletions(-) diff --git a/doc/source/tutorials/betweenness/assets/betweenness.py b/doc/source/tutorials/betweenness/assets/betweenness.py index f8409704c..8ede3ec46 100644 --- a/doc/source/tutorials/betweenness/assets/betweenness.py +++ b/doc/source/tutorials/betweenness/assets/betweenness.py @@ -3,30 +3,36 @@ import math import random -# Generate graph -random.seed(1) -g = ig.Graph.Barabasi(n=200, m=2) +def plot_betweenness(g, ax): + # Calculate vertex betweenness and scale it to be between 0.0 and 1.0 + vertex_betweenness = ig.rescale(g.betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/3)) + edge_betweenness = ig.rescale(g.edge_betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/2)) + + ig.plot( + g, + target=ax, + layout="fruchterman_reingold", + palette=ig.GradientPalette("white", "midnightblue"), + vertex_color=list(map(int, + ig.rescale(vertex_betweenness, (0, 255), clamp=True))), + edge_color=list(map(int, + ig.rescale(edge_betweenness, (0, 255), clamp=True))), + vertex_size=ig.rescale(vertex_betweenness, (0.1, 0.6)), + edge_width=ig.rescale(edge_betweenness, (0.5, 1.0)), + vertex_frame_width=0.2, + ) -# Calculate vertex betweenness and scale it to be between 0.0 and 1.0 -vertex_betweenness = ig.rescale(g.betweenness(), clamp=True, - scale=lambda x : math.pow(x, 1/3)) -edge_betweenness = ig.rescale(g.edge_betweenness(), clamp=True, - scale=lambda x : math.pow(x, 1/2)) +# Generate Krackhardt Kite Graphs and Barabasi graphs +random.seed(1) +g1 = ig.Graph.Famous("Krackhardt_Kite") +g2 = ig.Graph.Barabasi(n=200, m=2) # Plot the graph -fig, ax = plt.subplots() -ig.plot( - g, - target=ax, - layout="fruchterman_reingold", - palette=ig.GradientPalette("white", "midnightblue"), - vertex_color=list(map(int, - ig.rescale(vertex_betweenness, (0, 255), clamp=True))), - edge_color=list(map(int, - ig.rescale(edge_betweenness, (0, 255), clamp=True))), - vertex_size=ig.rescale(vertex_betweenness, (0.1, 0.6)), - edge_width=ig.rescale(edge_betweenness, (0.5, 1.0)), - vertex_frame_width=0.2, -) +fig, axs = plt.subplots(2, 1) +plot_betweenness(g1, axs[0]) +plot_betweenness(g2, axs[1]) + plt.show() diff --git a/doc/source/tutorials/betweenness/betweenness.rst b/doc/source/tutorials/betweenness/betweenness.rst index 63a8b48fd..a4c686fe7 100644 --- a/doc/source/tutorials/betweenness/betweenness.rst +++ b/doc/source/tutorials/betweenness/betweenness.rst @@ -11,7 +11,7 @@ Visualizing Betweenness .. _edge_betweenness: https://igraph.org/python/doc/api/igraph._igraph.GraphBase.html#edge_betweenness .. |edge_betweenness| replace:: :meth:`edge_betweenness` -This example will demonstrate how to visualize both vertex and edge betweenness with a custom defined color palette. We use the methods |betweenness|_ and |edge_betweenness|_ respectively. +This example will demonstrate how to visualize both vertex and edge betweenness with a custom defined color palette. We use the methods |betweenness|_ and |edge_betweenness|_ respectively, and demonstrate the effects on a standard `Krackhardt Kite `_ graph, as well as a `Barabási-Albert `_ random graph. .. code-block:: python @@ -20,39 +20,47 @@ This example will demonstrate how to visualize both vertex and edge betweenness import math import random - # Generate graph - random.seed(1) - g = ig.Graph.Barabasi(n=200, m=2) + def plot_betweenness(g, ax): + # Calculate vertex betweenness and scale it to be between 0.0 and 1.0 + vertex_betweenness = ig.rescale(g.betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/3)) + edge_betweenness = ig.rescale(g.edge_betweenness(), clamp=True, + scale=lambda x : math.pow(x, 1/2)) + + ig.plot( + g, + target=ax, + layout="fruchterman_reingold", + palette=ig.GradientPalette("white", "midnightblue"), + vertex_color=list(map(int, + ig.rescale(vertex_betweenness, (0, 255), clamp=True))), + edge_color=list(map(int, + ig.rescale(edge_betweenness, (0, 255), clamp=True))), + vertex_size=ig.rescale(vertex_betweenness, (0.1, 0.6)), + edge_width=ig.rescale(edge_betweenness, (0.5, 1.0)), + vertex_frame_width=0.2, + ) - # Calculate vertex betweenness and scale it to be between 0.0 and 1.0 - vertex_betweenness = ig.rescale(g.betweenness(), clamp=True, - scale=lambda x : math.pow(x, 1/3)) - edge_betweenness = ig.rescale(g.edge_betweenness(), clamp=True, - scale=lambda x : math.pow(x, 1/2)) + # Generate Krackhardt Kite Graphs and Barabasi graphs + random.seed(1) + g1 = ig.Graph.Famous("Krackhardt_Kite") + g2 = ig.Graph.Barabasi(n=200, m=2) # Plot the graph - fig, ax = plt.subplots() - ig.plot( - g, - target=ax, - layout="fruchterman_reingold", - palette=ig.GradientPalette("white", "midnightblue"), - vertex_color=list(map(int, - ig.rescale(vertex_betweenness, (0, 255), clamp=True))), - edge_color=list(map(int, - ig.rescale(edge_betweenness, (0, 255), clamp=True))), - vertex_size=ig.rescale(vertex_betweenness, (0.1, 0.6)), - edge_width=ig.rescale(edge_betweenness, (0.5, 1.0)), - vertex_frame_width=0.2, - ) - plt.show() + fig, axs = plt.subplots(2, 1) + plot_betweenness(g1, axs[0]) + plot_betweenness(g2, axs[1]) + plt.show() -Note that we scale the betweennesses for the vertices and edges by the cube root and square root respectively. The choice of scaling is arbitrary, but is used to give a smoother, more linear transition in the sizes and colors of nodes and edges. The final output graph is here: +.. _rescale: https://igraph.org/python/doc/api/igraph.utils.html#rescale +.. |rescale| replace:: :meth:`rescale` + +Here we use |rescale|_ as a great way to linearly scale all data into ranges we can work with. Note that we scale the betweennesses for the vertices and edges by the cube root and square root respectively. The choice of scaling is arbitrary, but is used to give a smoother, more linear transition in the sizes and colors of nodes and edges. The final output graphs are as follows: .. figure:: ./figures/betweenness.png :alt: A graph visualizing the betweenness of each vertex and edge. :align: center - A graph visualizing edge betweenness. White indicates a low betweenness centrality, whereas dark blue indicates a high betweenness centrality. + A graph visualizing edge betweenness in a Krackhardt Kite graph (left) and a 200 node Barabási-Albert graph (right). White indicates a low betweenness centrality, whereas dark blue indicates a high betweenness centrality. diff --git a/doc/source/tutorials/betweenness/figures/betweenness.png b/doc/source/tutorials/betweenness/figures/betweenness.png index a10c9fb2ec89bb70f46710e49067cad68e7fb3f9..10e4375c5a7bf525c059c2c9e087c39f5d19e5a9 100644 GIT binary patch literal 167428 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
  • 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 diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 71792d7ee..9befdfd75 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -48,6 +48,7 @@ This example is aimed at those with some familiarity with python and/or graphing edge_width=[2 if married else 1 for married in g.es["married"]], edge_color=["#F00" if married else "#000" for married in g.es["married"]], ) + ax.set_aspect(1) plt.show() From 9137698cf984667d041f33978f022371d0c6002a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 30 Nov 2021 15:51:50 +1100 Subject: [PATCH 0534/1681] Gallery page, links, TOC entries, and adaptations --- doc/source/gallery.rst | 16 ++++++++++++++++ doc/source/index.rst | 7 +++++++ doc/source/tutorial.rst | 2 ++ .../bipartite_matching/bipartite_matching.rst | 4 ++++ .../bipartite_matching_maxflow.rst | 4 ++++ doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 4 ++++ doc/source/tutorials/maxflow/maxflow.rst | 4 ++++ doc/source/tutorials/quickstart/quickstart.rst | 4 ++++ .../tutorials/shortest_paths/shortest_paths.rst | 4 ++++ 9 files changed, 49 insertions(+) create mode 100644 doc/source/gallery.rst diff --git a/doc/source/gallery.rst b/doc/source/gallery.rst new file mode 100644 index 000000000..517727fdc --- /dev/null +++ b/doc/source/gallery.rst @@ -0,0 +1,16 @@ +.. include:: include/global.rst + +.. gallery + +======== +Gallery +======== + +This page contains short examples showcasing the functionality of |igraph|: + + - :ref:`tutorials-quickstart` + - :ref:`tutorials-bipartite-matching` + - :ref:`tutorials-bipartite-matching-maxflow` + - :ref:`tutorials-random` + - :ref:`tutorials-maxflow` + - :ref:`tutorials-shortest-paths` diff --git a/doc/source/index.rst b/doc/source/index.rst index 3203cf801..1fd485c8d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -15,6 +15,13 @@ Contents: intro install tutorial + gallery + tutorials/quickstart/quickstart + tutorials/bipartite_matching/bipartite_matching + tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow + tutorials/erdos_renyi/erdos_renyi + tutorials/maxflow/maxflow + tutorials/shortest_paths/shortest_paths generation analysis visualisation diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 20c05bf4f..0c75b552a 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -16,6 +16,8 @@ start out. If you already have a stable programming background in other language a quick overview of Python, `Learn Python in 10 minutes `_ is probably your best bet. +.. note:: + For the impatient reader, see the :ref:`gallery` page for short, self-contained examples. Starting |igraph| ================= diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index 45f2b7cbf..ade2200d1 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -1,3 +1,7 @@ +.. include:: include/global.rst + +.. tutorials-bipartite-matching + ========================== Maximum Bipartite Matching ========================== diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index 3d00abd36..38e61cfaa 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -1,3 +1,7 @@ +.. include:: include/global.rst + +.. tutorials-bipartite-matching-maxflow + ========================================== Maximum Bipartite Matching by Maximum Flow ========================================== diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index 41da76d88..73ade4332 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -1,3 +1,7 @@ +.. include:: include/global.rst + +.. tutorials-random + ================= Erdős-Rényi Graph ================= diff --git a/doc/source/tutorials/maxflow/maxflow.rst b/doc/source/tutorials/maxflow/maxflow.rst index bf2bdefcb..5a078f296 100644 --- a/doc/source/tutorials/maxflow/maxflow.rst +++ b/doc/source/tutorials/maxflow/maxflow.rst @@ -1,3 +1,7 @@ +.. include:: include/global.rst + +.. tutorials-maxflow + ============ Maximum Flow ============ diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 9befdfd75..6f05a7184 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -1,3 +1,7 @@ +.. include:: include/global.rst + +.. tutorials-quickstart + =========== Quick Start =========== diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index 21834b3d1..6b80609a2 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -1,3 +1,7 @@ +.. include:: include/global.rst + +.. tutorials-shortest-paths + ============== Shortest Paths ============== From 523f97ad1de7587db8afcc174228931208620e55 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 1 Dec 2021 06:39:14 +1100 Subject: [PATCH 0535/1681] Review changes --- .../assets/bipartite_matching.py | 6 +++--- .../bipartite_matching/bipartite_matching.rst | 6 +++--- .../assets/bipartite_matching_maxflow.py | 21 +++++++------------ .../bipartite_matching_maxflow.rst | 19 +++++++---------- .../tutorials/quickstart/quickstart.rst | 13 ++++++------ .../shortest_paths/assets/shortest_path.py | 6 ++---- 6 files changed, 30 insertions(+), 41 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py index c4d487f69..0b7483e8d 100644 --- a/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py @@ -6,7 +6,7 @@ [0, 0, 0, 0, 0, 1, 1, 1, 1], [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) -assert(g.is_bipartite()) +assert g.is_bipartite() matching = g.maximum_bipartite_matching() @@ -15,7 +15,7 @@ print("Matching is:") for i in range(5): print(f"{i} - {matching.match_of(i)}") - if matching.match_of(i): + if matching.match_of(i) is not None: matching_size += 1 print("Size of Maximum Matching is:", matching_size) @@ -27,7 +27,7 @@ vertex_size=0.4, vertex_label=range(g.vcount()), vertex_color="lightblue", - edge_width=[2.5 if e.target==matching.match_of(e.source) else 1.0 for e in g.es] + edge_width=[2.5 if e.target == matching.match_of(e.source) else 1.0 for e in g.es] ) ax.set_aspect(1) diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index ade2200d1..c7b606454 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -18,7 +18,7 @@ This example demonstrates an efficient way to find and visualise a maximum bipar [0, 0, 0, 0, 0, 1, 1, 1, 1], [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) - assert(g.is_bipartite()) + assert g.is_bipartite() matching = g.maximum_bipartite_matching() @@ -33,7 +33,7 @@ Then run the maximum matching, print("Matching is:") for i in range(5): print(f"{i} - {matching.match_of(i)}") - if matching.match_of(i): + if matching.match_of(i) is not None: matching_size += 1 print("Size of Maximum Matching is:", matching_size) @@ -50,7 +50,7 @@ And finally display the bipartite graph with matchings highlighted. vertex_size=0.4, vertex_label=range(g.vcount()), vertex_color="lightblue", - edge_width=[2.5 if e.target==matching.match_of(e.source) else 1.0 for e in g.es] + edge_width=[2.5 if e.target == matching.match_of(e.source) else 1.0 for e in g.es] ) ax.set_aspect(1) plt.show() diff --git a/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py index d75ec7c14..6b7624dfb 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py +++ b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py @@ -8,29 +8,24 @@ ) # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side -for i in range(4): - g.vs[i]["type"] = True -for i in range(4, 9): - g.vs[i]["type"] = False +g.vs[range(4)]["type"] = True +g.vs[range(4, 9)]["type"] = False # Add source and sink as nodes 9 and 10 g.add_vertices(2) -g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side -g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other +g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side +g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other flow = g.maxflow(9, 10) -print("Size of Maximum Matching (maxflow) is:", flow.value) +print("Size of maximum matching (maxflow) is:", flow.value) # Compare this to the "maximum_bipartite_matching()" function g2 = g.copy() -g2.delete_vertices([9, 10]) # delete the source and sink, which are unneeded for this function. +g2.delete_vertices([9, 10]) # delete the source and sink, which are unneeded for this function. matching = g2.maximum_bipartite_matching() -matching_size = 0 -for i in range(4): - if matching.match_of(i): - matching_size += 1 -print("Size of Maximum Matching (maximum_bipartite_matching) is:", matching_size) +matching_size = sum(1 for i in range(4) if matching.is_matched(i)) +print("Size of maximum matching (maximum_bipartite_matching) is:", matching_size) # Manually set the position of source and sink to display nicely layout = g.layout_bipartite() diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index 38e61cfaa..b82c4980d 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -6,7 +6,9 @@ Maximum Bipartite Matching by Maximum Flow ========================================== -This example presents how to visualise bipartite matching using maximum flow. Please note that the *igraph* already has :meth:`maximum_bipartite_matching` which is better suited for finding the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching<>`_. This particular example is purely for demonstrative purposes. +This example presents how to visualise bipartite matching using maximum flow. + +.. note:: :meth:`maximum_bipartite_matching` is usually a better way to find the maximum bipartite matching. For a demonstration on how to use that method instead, check out `Maximum Bipartite Matching<>`_. .. TODO: add link to Maximum Bipartite Matching @@ -23,17 +25,15 @@ This example presents how to visualise bipartite matching using maximum flow. Pl ) # Assign nodes 0-3 to one side, and the nodes 4-8 to the other side - for i in range(4): - g.vs[i]["type"] = True - for i in range(4, 9): - g.vs[i]["type"] = False + g.vs[range(4)]["type"] = True + g.vs[range(4, 9)]["type"] = False g.add_vertices(2) g.add_edges([(9, 0), (9, 1), (9, 2), (9, 3)]) # connect source to one side g.add_edges([(4, 10), (5, 10), (6, 10), (7, 10), (8, 10)]) # ... and sinks to the other flow = g.maxflow(9, 10) # not setting capacities means that all edges have capacity 1.0 - print("Size of Maximum Matching (maxflow) is:", flow.value) + print("Size of maximum matching (maxflow) is:", flow.value) Let's compare the output against :meth:`maximum_bipartite_matching` @@ -45,11 +45,8 @@ Let's compare the output against :meth:`maximum_bipartite_matching` matching = g2.maximum_bipartite_matching() - matching_size = 0 - for i in range(4): - if matching.match_of(i): - matching_size += 1 - print("Size of Maximum Matching (maximum_bipartite_matching) is:", matching_size) + matching_size = sum(1 for i in range(4) if matching.is_matched(i)) + print("Size of maximum matching (maximum_bipartite_matching) is:", matching_size) And finally, display the original flow graph nicely with the matchings added diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index 6f05a7184..74ba5c7e9 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -5,16 +5,15 @@ =========== Quick Start =========== +For the eager folks out there, this intro will give you a quick overview of the following operations: -This quick start will demonstrate the following: - -- Construct a *igraph* graph from scratch -- Set the attributes of nodes and edges -- Plot out a graph using matplotlib -- Save a graph as an image +- Construct a graph +- Set attributes of nodes and edges +- Plot a graph using matplotlib +- Save the plot as an image - Export and import a graph as a ``.gml`` file -This example is aimed at those with some familiarity with python and/or graphing packages, who would like to get some code up and running as fast as possible. +Check out our in-depth tutorial TODO LINK and our gallery TODO LINK for more. .. code-block:: python diff --git a/doc/source/tutorials/shortest_paths/assets/shortest_path.py b/doc/source/tutorials/shortest_paths/assets/shortest_path.py index 1234c1e14..607854ed1 100644 --- a/doc/source/tutorials/shortest_paths/assets/shortest_path.py +++ b/doc/source/tutorials/shortest_paths/assets/shortest_path.py @@ -24,13 +24,11 @@ if len(results[0]) > 0: # Add up the weights across all edges on the shortest path - distance = 0 - for e in results[0]: - distance += g.es[e]["weight"] + distance = sum(g.es[e]["weight"] for e in results[0]) print("Shortest weighted distance is: ", distance) else: print("End node could not be reached!") # Output: # Shortest distance is: 3 -# Shortest weighted distance is: 8 \ No newline at end of file +# Shortest weighted distance is: 8 From 85b884126b6ba02263ebbf37723979c27f779205 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 1 Dec 2021 07:29:07 +1100 Subject: [PATCH 0536/1681] Stub for ring animation tutorial --- .../ring_animation/assets/ring_animation.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 doc/source/tutorials/ring_animation/assets/ring_animation.py diff --git a/doc/source/tutorials/ring_animation/assets/ring_animation.py b/doc/source/tutorials/ring_animation/assets/ring_animation.py new file mode 100644 index 000000000..a4dc0062d --- /dev/null +++ b/doc/source/tutorials/ring_animation/assets/ring_animation.py @@ -0,0 +1,27 @@ +import igraph as ig +import matplotlib.pyplot as plt + +# Animate a directed ring graph +g = ig.Graph.Ring(10, directed=True) + +# Make 2D ring layout +layout = g.layout_circle() + +# Create canvas +fig, ax = plt.subplots() + + +# Prepare interactive backend for autoupdate +plt.ion() +plt.show() + +# Animate, one vertex at a time +for frame in range(11): + ax.clear() + # Fix limits (unless you want a zoom-out effect) + ax.set_xlim(-1.5, 1.5) + ax.set_ylim(-1.5, 1.5) + gd = g.subgraph(range(frame)) + ig.plot(gd, target=ax, layout=layout[:frame]) + fig.canvas.draw_idle() + fig.canvas.start_event_loop(0.5) From 2f3b4b6495c73480c02eda6ba5d5fa68a6827f00 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 1 Dec 2021 07:32:39 +1100 Subject: [PATCH 0537/1681] Better line comments in ring tutorial --- .../tutorials/ring_animation/assets/ring_animation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/source/tutorials/ring_animation/assets/ring_animation.py b/doc/source/tutorials/ring_animation/assets/ring_animation.py index a4dc0062d..951337ff6 100644 --- a/doc/source/tutorials/ring_animation/assets/ring_animation.py +++ b/doc/source/tutorials/ring_animation/assets/ring_animation.py @@ -10,18 +10,23 @@ # Create canvas fig, ax = plt.subplots() - # Prepare interactive backend for autoupdate plt.ion() plt.show() # Animate, one vertex at a time for frame in range(11): + # Remove previous plot elements ax.clear() + # Fix limits (unless you want a zoom-out effect) ax.set_xlim(-1.5, 1.5) ax.set_ylim(-1.5, 1.5) + + # Plot subgraph gd = g.subgraph(range(frame)) ig.plot(gd, target=ax, layout=layout[:frame]) + + # matplotlib animation infrastructure fig.canvas.draw_idle() fig.canvas.start_event_loop(0.5) From 6191662c37012953bb4e8112a3860ef804024578 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 1 Dec 2021 14:31:17 +0100 Subject: [PATCH 0538/1681] style: nitpicking --- .../bipartite_matching/assets/bipartite_matching.py | 6 +++--- .../tutorials/bipartite_matching/bipartite_matching.rst | 6 +++--- .../bipartite_matching_maxflow.rst | 4 ++-- doc/source/tutorials/shortest_paths/assets/shortest_path.py | 4 ++-- doc/source/tutorials/shortest_paths/shortest_paths.rst | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py index 0b7483e8d..c92c64800 100644 --- a/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py @@ -15,9 +15,9 @@ print("Matching is:") for i in range(5): print(f"{i} - {matching.match_of(i)}") - if matching.match_of(i) is not None: + if matching.is_matched(i): matching_size += 1 -print("Size of Maximum Matching is:", matching_size) +print("Size of maximum matching is:", matching_size) fig, ax = plt.subplots(figsize=(7, 3)) ig.plot( @@ -39,4 +39,4 @@ # 2 - 8 # 3 - 6 # 4 - None -# Size of Maximum Matching is: 4 +# Size of maximum matching is: 4 diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index c7b606454..d2bd96527 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -33,9 +33,9 @@ Then run the maximum matching, print("Matching is:") for i in range(5): print(f"{i} - {matching.match_of(i)}") - if matching.match_of(i) is not None: + if matching.is_matched(i): matching_size += 1 - print("Size of Maximum Matching is:", matching_size) + print("Size of maximum matching is:", matching_size) And finally display the bipartite graph with matchings highlighted. @@ -65,7 +65,7 @@ The received output is 2 - 8 3 - 6 4 - None - Size of Maximum Matching is: 4 + Size of maximum matching is: 4 .. figure:: ./figures/bipartite.png :alt: The visual representation of maximal bipartite matching diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index b82c4980d..a88f209b4 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -74,8 +74,8 @@ The received output is: .. code-block:: - Size of Maximum Matching (maxflow) is: 4.0 - Size of Maximum Matching (maximum_bipartite_matching) is: 4 + Size of maximum matching (maxflow) is: 4.0 + Size of maximum matching (maximum_bipartite_matching) is: 4 .. figure:: ./figures/bipartite_matching_maxflow.png :alt: The visual representation of maximal bipartite matching diff --git a/doc/source/tutorials/shortest_paths/assets/shortest_path.py b/doc/source/tutorials/shortest_paths/assets/shortest_path.py index 607854ed1..e79b63315 100644 --- a/doc/source/tutorials/shortest_paths/assets/shortest_path.py +++ b/doc/source/tutorials/shortest_paths/assets/shortest_path.py @@ -8,7 +8,7 @@ ) # g.get_shortest_paths() returns a list of vertex ID paths -results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] +results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] if len(results[0]) > 0: # The distance is the number of vertices in the shortest path minus one. @@ -20,7 +20,7 @@ g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] # g.get_shortest_paths() returns a list of edge ID paths -results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] +results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] if len(results[0]) > 0: # Add up the weights across all edges on the shortest path diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index 6b80609a2..dc83871c4 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -22,7 +22,7 @@ To find the shortest path or distance between two nodes, we can use :meth:`get_s ) # g.get_shortest_paths() returns a list of vertex ID paths - results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] + results = g.get_shortest_paths(1, to=4, output="vpath") # results = [[1, 0, 2, 4]] if len(results[0]) > 0: # The distance is the number of vertices in the shortest path minus one. @@ -38,7 +38,7 @@ If the edges have associated distances or weights, we pass them in as an argumen g.es["weight"] = [2, 1, 5, 4, 7, 3, 2] # g.get_shortest_paths() returns a list of edge ID paths - results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] + results = g.get_shortest_paths(0, to=5, weights=g.es["weight"], output="epath") # results = [[1, 3, 5]] if len(results[0]) > 0: # Add up the weights across all edges on the shortest path From 43157403d814ba1ac2554fd4164f7ba40a5f6299 Mon Sep 17 00:00:00 2001 From: h5jam Date: Thu, 2 Dec 2021 21:48:57 +0900 Subject: [PATCH 0539/1681] add: topological sort example --- .../assets/topological_sort.py | 17 +++++ .../figures/topological_sort.png | Bin 0 -> 19017 bytes .../topological_sort/topological_sort.rst | 59 ++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 doc/source/tutorials/topological_sort/assets/topological_sort.py create mode 100644 doc/source/tutorials/topological_sort/figures/topological_sort.png create mode 100644 doc/source/tutorials/topological_sort/topological_sort.rst diff --git a/doc/source/tutorials/topological_sort/assets/topological_sort.py b/doc/source/tutorials/topological_sort/assets/topological_sort.py new file mode 100644 index 000000000..02b384b5e --- /dev/null +++ b/doc/source/tutorials/topological_sort/assets/topological_sort.py @@ -0,0 +1,17 @@ +import igraph as ig + +# generate directed acyclic graph +g = ig.Graph(edges=[(0, 1), (0, 2), (1, 3), (2, 4), (4, 3), (3, 5), (4, 5)], + directed=True) +assert g.is_dag + +# topological sorting +results = g.topological_sorting(mode='out') +print('Topological sorting result (out):', *results) + +results = g.topological_sorting(mode='in') +print('Topological sorting result (in):', *results) + +# print indegree of each node +for i in range(g.vcount()): + print('degree of {}: {}'.format(i, g.vs[i].indegree())) \ No newline at end of file diff --git a/doc/source/tutorials/topological_sort/figures/topological_sort.png b/doc/source/tutorials/topological_sort/figures/topological_sort.png new file mode 100644 index 0000000000000000000000000000000000000000..24ca49711547068bf574cec820a06ecf723532cb GIT binary patch literal 19017 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 aJ^9s}$EoRX8h+LZrJ Date: Thu, 2 Dec 2021 22:18:56 +0900 Subject: [PATCH 0540/1681] fix: topological_sort.rst --- doc/source/tutorials/topological_sort/topological_sort.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/tutorials/topological_sort/topological_sort.rst b/doc/source/tutorials/topological_sort/topological_sort.rst index 11c881d58..6861f7a67 100644 --- a/doc/source/tutorials/topological_sort/topological_sort.rst +++ b/doc/source/tutorials/topological_sort/topological_sort.rst @@ -26,7 +26,7 @@ To get topological sorted list, we can use :meth:`topological_sorting`. If given results = g.topological_sorting(mode='in') print('Topological sorting result (in):', *results) -Default mode is `out`, start from the first node with in-degree as 0. And mode is `in`, start from the first node with in-degree as maximum of all degrees. +Default mode is 'out', start from the first node with in-degree as 0. And mode is 'in', start from the first node with in-degree as maximum of all degrees. .. code-block:: python From 47412e7a0531055b111262dd6c1153d5dd353aef Mon Sep 17 00:00:00 2001 From: ah00ee Date: Fri, 3 Dec 2021 02:43:09 +0900 Subject: [PATCH 0541/1681] Add topological_sort.rst --- doc/source/tutorials/topological_sort/topological_sort.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/tutorials/topological_sort/topological_sort.rst b/doc/source/tutorials/topological_sort/topological_sort.rst index 6861f7a67..60f5e6178 100644 --- a/doc/source/tutorials/topological_sort/topological_sort.rst +++ b/doc/source/tutorials/topological_sort/topological_sort.rst @@ -6,7 +6,7 @@ Topological Sort ================ -This example show how to sort topological order with directed acyclic graph (DAG). +This example show how to sort by topological order with directed acyclic graph(DAG). To get topological sorted list, we can use :meth:`topological_sorting`. If given graph is not DAG, error will be returned. @@ -26,7 +26,7 @@ To get topological sorted list, we can use :meth:`topological_sorting`. If given results = g.topological_sorting(mode='in') print('Topological sorting result (in):', *results) -Default mode is 'out', start from the first node with in-degree as 0. And mode is 'in', start from the first node with in-degree as maximum of all degrees. +There are two modes for :meth:`topological_sorting`. Default mode is 'out', which starts from the node with zero in-degree to sort nodes by topological order. The other mode, 'in', starts from the node with maximum in-degree. .. code-block:: python @@ -56,4 +56,4 @@ The output of two sorted list following as: The graph `g` -- Note that :meth:`topological_sorting` returns topological sorted list and we can set a mode of two. \ No newline at end of file +- Note that :meth:`topological_sorting` returns topological sorted list and we can set a mode of two. From 858ca8a8738ac36cd26dc13126d9a23d89fa2f64 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Thu, 2 Dec 2021 19:31:44 +0100 Subject: [PATCH 0542/1681] test_atlas: skip more graphs. --- tests/test_atlas.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_atlas.py b/tests/test_atlas.py index 546a28300..5041795dc 100644 --- a/tests/test_atlas.py +++ b/tests/test_atlas.py @@ -38,9 +38,6 @@ def testEigenvectorCentrality(self): try: for idx, g in enumerate(self.__class__.graphs): - if idx in self.__class__.skip_graphs: - # Skip this graph; it causes lots of problems and I don't know why - continue try: ec, eval = g.evcent(return_eigenvalue=True) @@ -156,14 +153,18 @@ def testAuthorityScore(self): class GraphAtlasTests(unittest.TestCase, AtlasTestBase): graphs = [Graph.Atlas(i) for i in range(1253)] - skip_graphs = set([180]) +# Skip some problematic graphs +GraphAtlasTests.graphs = [g for idx, g in enumerate(GraphAtlasTests.graphs) if idx not in set([70, 180])] +print(len(GraphAtlasTests.graphs)) class IsoclassTests(unittest.TestCase, AtlasTestBase): graphs = [Graph.Isoclass(3, i, directed=True) for i in range(16)] + [ Graph.Isoclass(4, i, directed=True) for i in range(218) ] - skip_graphs = set([136]) + +# Skip some problematic graphs +IsoclassTests.graphs = [g for idx, g in enumerate(IsoclassTests.graphs) if idx not in set([136])] def suite(): From ceba21dce391d046b8b862df82fd0c4db475370b Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Fri, 3 Dec 2021 14:04:47 +1100 Subject: [PATCH 0543/1681] Fix spacing in erdos_renyi.rst --- doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index 73ade4332..bdcee6db8 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -82,18 +82,17 @@ The received output is: IGRAPH U--- 20 35 -- .. figure:: ./figures/erdos_renyi_p.png + :alt: The visual representation of a randomly generated Erdos Renyi graph :align: center Erdos Renyi random graphs with probability ``p`` = 0.2 .. figure:: ./figures/erdos_renyi_m.png + :alt: The second visual representation of a randomly generated Erdos Renyi graph :align: center Erdos Renyi random graphs with ``m`` = 35 edges Note that even when using the same random seed, results can still differ depending on the machine the code is being run from. - - - From 60db6f2214565b582ad3d039eae5cdc35ca395e9 Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Fri, 3 Dec 2021 14:05:21 +1100 Subject: [PATCH 0544/1681] Add ring_animation.rst --- doc/source/index.rst | 1 + .../ring_animation/assets/ring_animation.py | 5 +- .../ring_animation/figures/ring_animation.gif | Bin 0 -> 39803 bytes .../ring_animation/ring_animation.rst | 55 ++++++++++++++++++ 4 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 doc/source/tutorials/ring_animation/figures/ring_animation.gif create mode 100644 doc/source/tutorials/ring_animation/ring_animation.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index 1fd485c8d..e2295739c 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -21,6 +21,7 @@ Contents: tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow tutorials/erdos_renyi/erdos_renyi tutorials/maxflow/maxflow + tutorials/ring_animation/ring_animation tutorials/shortest_paths/shortest_paths generation analysis diff --git a/doc/source/tutorials/ring_animation/assets/ring_animation.py b/doc/source/tutorials/ring_animation/assets/ring_animation.py index 951337ff6..02e028ec5 100644 --- a/doc/source/tutorials/ring_animation/assets/ring_animation.py +++ b/doc/source/tutorials/ring_animation/assets/ring_animation.py @@ -9,6 +9,7 @@ # Create canvas fig, ax = plt.subplots() +ax.set_aspect(1) # Prepare interactive backend for autoupdate plt.ion() @@ -16,7 +17,7 @@ # Animate, one vertex at a time for frame in range(11): - # Remove previous plot elements + # Remove plot elements from the previous frame ax.clear() # Fix limits (unless you want a zoom-out effect) @@ -25,7 +26,7 @@ # Plot subgraph gd = g.subgraph(range(frame)) - ig.plot(gd, target=ax, layout=layout[:frame]) + ig.plot(gd, target=ax, layout=layout[:frame], vertex_color="yellow") # matplotlib animation infrastructure fig.canvas.draw_idle() diff --git a/doc/source/tutorials/ring_animation/figures/ring_animation.gif b/doc/source/tutorials/ring_animation/figures/ring_animation.gif new file mode 100644 index 0000000000000000000000000000000000000000..613dcfaac38c459042276ca9cabb0b862b779656 GIT binary patch literal 39803 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 literal 0 HcmV?d00001 diff --git a/doc/source/tutorials/ring_animation/ring_animation.rst b/doc/source/tutorials/ring_animation/ring_animation.rst new file mode 100644 index 000000000..92646a9d0 --- /dev/null +++ b/doc/source/tutorials/ring_animation/ring_animation.rst @@ -0,0 +1,55 @@ +.. include:: ../../include/global.rst + +.. tutorials-ring-animation + +==================== +Ring Graph Animation +==================== + +This example demonstrates how to use Matplotlib's `animation features `_ in order to animate a ring graph sequentially being revealed. + +.. code-block:: python + + import igraph as ig + import matplotlib.pyplot as plt + + # Animate a directed ring graph + g = ig.Graph.Ring(10, directed=True) + + # Make 2D ring layout + layout = g.layout_circle() + + # Create canvas + fig, ax = plt.subplots() + ax.set_aspect(1) + + # Prepare interactive backend for autoupdate + plt.ion() + plt.show() + + # Animate, one vertex at a time + for frame in range(11): + # Remove plot elements from the previous frame + ax.clear() + + # Fix limits (unless you want a zoom-out effect) + ax.set_xlim(-1.5, 1.5) + ax.set_ylim(-1.5, 1.5) + + # Plot subgraph + gd = g.subgraph(range(frame)) + ig.plot(gd, target=ax, layout=layout[:frame], vertex_color="yellow") + + # matplotlib animation infrastructure + fig.canvas.draw_idle() + fig.canvas.start_event_loop(0.5) + +The received output is: + +.. figure:: ./figures/ring_animation.gif + + :alt: The visualisation of a animated ring graph + :align: center + :caption: Sequentially animated ring graph. + +Note that we use *igraph*'s :meth:`Graph.subgraph()` (a.k.a `:meth:`Graph.induced_subgraph()``_) in order to obtain a section of the ring graph at a time for each frame. From 0093ea53f441b1655dae06bc4fb70c8794050cfa Mon Sep 17 00:00:00 2001 From: Gomango999 Date: Fri, 3 Dec 2021 16:02:18 +1100 Subject: [PATCH 0545/1681] Use ".. note" across all tutorials --- .../bipartite_matching_maxflow.rst | 4 +++- doc/source/tutorials/erdos_renyi/erdos_renyi.rst | 5 ++++- doc/source/tutorials/ring_animation/ring_animation.rst | 5 ++++- doc/source/tutorials/shortest_paths/shortest_paths.rst | 7 +++++-- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index a88f209b4..a53a1b42a 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -83,4 +83,6 @@ The received output is: Maximal Bipartite Matching -Note that maximum flow will represent the capacities as real values, which is why our result is ``4.0`` instead of ``4``. +.. note:: + + Maximum flow will represent the capacities as real values, which is why our result is ``4.0`` instead of ``4``. diff --git a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst index bdcee6db8..d4a451e5f 100644 --- a/doc/source/tutorials/erdos_renyi/erdos_renyi.rst +++ b/doc/source/tutorials/erdos_renyi/erdos_renyi.rst @@ -95,4 +95,7 @@ The received output is: Erdos Renyi random graphs with ``m`` = 35 edges -Note that even when using the same random seed, results can still differ depending on the machine the code is being run from. + +.. note:: + + Even when using the same random seed, results can still differ depending on the machine the code is being run from. diff --git a/doc/source/tutorials/ring_animation/ring_animation.rst b/doc/source/tutorials/ring_animation/ring_animation.rst index 92646a9d0..9add9a945 100644 --- a/doc/source/tutorials/ring_animation/ring_animation.rst +++ b/doc/source/tutorials/ring_animation/ring_animation.rst @@ -52,4 +52,7 @@ The received output is: :align: center :caption: Sequentially animated ring graph. -Note that we use *igraph*'s :meth:`Graph.subgraph()` (a.k.a `:meth:`Graph.induced_subgraph()``_) in order to obtain a section of the ring graph at a time for each frame. + +.. note:: + + We use *igraph*'s :meth:`Graph.subgraph()` (a.k.a `Graph.induced_subgraph()``_) in order to obtain a section of the ring graph at a time for each frame. diff --git a/doc/source/tutorials/shortest_paths/shortest_paths.rst b/doc/source/tutorials/shortest_paths/shortest_paths.rst index dc83871c4..c7623fc0c 100644 --- a/doc/source/tutorials/shortest_paths/shortest_paths.rst +++ b/doc/source/tutorials/shortest_paths/shortest_paths.rst @@ -64,8 +64,11 @@ The output of these these two shortest paths are: .. TODO: Add in edge weights when possible! Matplotlib does not support displaying edge weights (and the develop branch implementation is bugged). -- Note that :meth:`get_shortest_paths` returns a list of lists becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. -- If you're interested in finding *all* shortest paths, take a look at :meth:`get_all_shortest_paths`. + +.. note:: + + - :meth:`get_shortest_paths` returns a list of lists becuase the `to` argument can also accept a list of vertex IDs. In that case, the shortest path to all each vertex is found and stored in the results array. + - If you're interested in finding *all* shortest paths, take a look at :meth:`get_all_shortest_paths`. From 096168d58067e403fbf6227129e25601e0acace9 Mon Sep 17 00:00:00 2001 From: ah00ee Date: Fri, 3 Dec 2021 14:42:58 +0900 Subject: [PATCH 0546/1681] doc: fix: grammer in topological_sort.rst --- .../topological_sort/topological_sort.rst | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/doc/source/tutorials/topological_sort/topological_sort.rst b/doc/source/tutorials/topological_sort/topological_sort.rst index 60f5e6178..09e92fc2e 100644 --- a/doc/source/tutorials/topological_sort/topological_sort.rst +++ b/doc/source/tutorials/topological_sort/topological_sort.rst @@ -6,54 +6,62 @@ Topological Sort ================ -This example show how to sort by topological order with directed acyclic graph(DAG). - -To get topological sorted list, we can use :meth:`topological_sorting`. If given graph is not DAG, error will be returned. +To get a topological sort of directed acyclic graph(DAG), we can use :meth:`topological_sortng`. .. code-block:: python import igraph as ig - # generate directed acyclic graph + # generate directed acyclic graph(DAG) g = ig.Graph(edges=[(0, 1), (0, 2), (1, 3), (2, 4), (4, 3), (3, 5), (4, 5)], directed=True) assert g.is_dag - # topological sorting - results = g.topological_sorting(mode='out') - print('Topological sorting result (out):', *results) + # g.topological_sorting() returns a list of vertex ID paths. + # If the given graph is not DAG, error will be returned. + results = g.topological_sorting(mode='out') # results = [0, 1, 2, 4, 3, 5] + print('Topological sort of graph g on 'out' mode:', *results) + + results = g.topological_sorting(mode='in') # results = [5, 3, 1, 4, 2, 0] + print('Topological sort of graph g on 'in' mode:', *results) + +There are two modes of :meth:`topological_sorting`. Default mode is 'out', it starts a topological sorting from the node with indegree 0. The other mode is 'in', it starts a topological sorting from the node that has maximum indegree. + +The output of the code above is: + +.. code-block:: - results = g.topological_sorting(mode='in') - print('Topological sorting result (in):', *results) + Topological sort of graph g on 'out' mode: 0 1 2 4 3 5 + Topological sort of graph g on 'in' mode: 5 3 1 4 2 0 -There are two modes for :meth:`topological_sorting`. Default mode is 'out', which starts from the node with zero in-degree to sort nodes by topological order. The other mode, 'in', starts from the node with maximum in-degree. +For finding indegree of each node, we can use :meth:`indegree()`. .. code-block:: python import igraph as ig - # generate directed acyclic graph + # generate directed acyclic graph(DAG) g = ig.Graph(edges=[(0, 1), (0, 2), (1, 3), (2, 4), (4, 3), (3, 5), (4, 5)], directed=True) - # print indegree of each node + # g.vs[i].indegree() returns the indegree of each vertex(which is g.vs[i]). for i in range(g.vcount()): print('degree of {}: {}'.format(i, g.vs[i].indegree())) -We can use :meth:`indegree()` to compute indegree of a node. - -The output of two sorted list following as: - -.. code-block:: - - Topological sorting is (out): 0 1 2 4 3 5 - Topological sorting is (in): 5 3 1 4 2 0 + ''' + degree of 0: 0 + degree of 1: 1 + degree of 2: 2 + degree of 3: 3 + degree of 4: 4 + degree of 5: 5 + ''' .. figure:: ./figures/topological_sort.png - :alt: The visual representation of a directed acyclic graph for topological sorting + :alt: The visual representation of a directed acyclic graph(DAG) for topological sorting :align: center The graph `g` -- Note that :meth:`topological_sorting` returns topological sorted list and we can set a mode of two. +- Note that :meth:`topological_sorting` returns a list of vertice ID paths and we can set two modes. From dc25f5714c9d38c5f3dcdc80ba8e90aed8457129 Mon Sep 17 00:00:00 2001 From: "Yesung(Isaac) Lee" <49810053+Isaac-Lee@users.noreply.github.com> Date: Fri, 3 Dec 2021 15:03:25 +0900 Subject: [PATCH 0547/1681] add graph_traversal.rst --- .../tutorials/topological_sort/graph_traversal.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 doc/source/tutorials/topological_sort/graph_traversal.rst diff --git a/doc/source/tutorials/topological_sort/graph_traversal.rst b/doc/source/tutorials/topological_sort/graph_traversal.rst new file mode 100644 index 000000000..40fd31a78 --- /dev/null +++ b/doc/source/tutorials/topological_sort/graph_traversal.rst @@ -0,0 +1,13 @@ +.. include:: include/global.rst + +.. tutorials-graph-graversal + +================ +Graph Traversal +================ + +To get a topological sort of directed acyclic graph(DAG), we can use :meth:`topological_sortng`. + +.. code-block:: python + + import igraph as ig From 9f3f3a578a8b605d1535883d999904de1aca7d84 Mon Sep 17 00:00:00 2001 From: "Yesung(Isaac) Lee" <49810053+Isaac-Lee@users.noreply.github.com> Date: Fri, 3 Dec 2021 15:07:40 +0900 Subject: [PATCH 0548/1681] doc: fix: graph_traversal.rst --- doc/source/tutorials/topological_sort/graph_traversal.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/doc/source/tutorials/topological_sort/graph_traversal.rst b/doc/source/tutorials/topological_sort/graph_traversal.rst index 40fd31a78..d96f4822a 100644 --- a/doc/source/tutorials/topological_sort/graph_traversal.rst +++ b/doc/source/tutorials/topological_sort/graph_traversal.rst @@ -5,9 +5,3 @@ ================ Graph Traversal ================ - -To get a topological sort of directed acyclic graph(DAG), we can use :meth:`topological_sortng`. - -.. code-block:: python - - import igraph as ig From 9b41460b487b847cfa96fdc6dd70328979b546ed Mon Sep 17 00:00:00 2001 From: "Yesung(Isaac) Lee" <49810053+Isaac-Lee@users.noreply.github.com> Date: Fri, 3 Dec 2021 15:08:07 +0900 Subject: [PATCH 0549/1681] doc: fix: graph_traversal.rst From 41523d9c8479af132bf75c7fbb8ee5c7889ad5c9 Mon Sep 17 00:00:00 2001 From: isaac_lee Date: Fri, 3 Dec 2021 15:12:04 +0900 Subject: [PATCH 0550/1681] delete file --- doc/source/tutorials/topological_sort/graph_traversal.rst | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 doc/source/tutorials/topological_sort/graph_traversal.rst diff --git a/doc/source/tutorials/topological_sort/graph_traversal.rst b/doc/source/tutorials/topological_sort/graph_traversal.rst deleted file mode 100644 index d96f4822a..000000000 --- a/doc/source/tutorials/topological_sort/graph_traversal.rst +++ /dev/null @@ -1,7 +0,0 @@ -.. include:: include/global.rst - -.. tutorials-graph-graversal - -================ -Graph Traversal -================ From 9dbabbd50aac195ec1ac4eb317cea5e12433e2a9 Mon Sep 17 00:00:00 2001 From: h5jam Date: Sun, 12 Dec 2021 15:24:21 +0900 Subject: [PATCH 0551/1681] doc: fix a figure of topological sort --- .../figures/topological_sort.png | Bin 19017 -> 23883 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/source/tutorials/topological_sort/figures/topological_sort.png b/doc/source/tutorials/topological_sort/figures/topological_sort.png index 24ca49711547068bf574cec820a06ecf723532cb..db31031e7bfb36722696d15303bb98baf2781587 100644 GIT binary patch literal 23883 zcmeFZ^;cDG^gW6Sf*{=uhmvkVQfU!Tkdh85LAq1o&>e!ky(#~t?{xIbLSc*i>iu+QH6dDgSmTyxHKV(w|F65`Y0V_{(t-d0o6!NR(* zi~NUo8Gh0|IvorDyWy#9=y~78*3-w*-3Ck3($m$+#nZ{bipAT;-NV7fSyVtwK!lIQ z-qX|7LrPHa@&EM)0xs@$g1q5#hVUU*T-6?UU||tkBL7_|lrM0=!ZNA4t#nJ*H)H*m zkFKskF3y(hE((t)TBltBTM4JFaB%!ie7yx(Eh*vkHLFd!J7rewWY%l-KAIY0_~T6_ zc(K>QSn7Lwj@h#7Xzo~~zm)c{J0Jaal+`-vA>kLL-2R#H+zH7RhRP-GXVq)17V z@yh=HOa8yVVwV+gSx`F9O$0jQs0E+CdKEi4X+lFsm!n&xcir0xdsjeh^2_m!=gW^mLSm7hS(sn0IQY6t z&?V)%%)0Fo)&=fUcvJGQSj^nK${YB@!F;0j0_(^H0$%*sE5|#$bcU|2*Ti$BY55Bo|iG!1qj0z%M ztncjfc-yo;>za+;W%(X6>{5Ti)a(17I*Cc7+$c7tqubVgd?ICFF!}v12&LJ9JnBcA zZmQk&32HCnKs2jIaF|1pj=Fr5E>C+&s=TFvJ)TMuQv#ZR`s4T8f81V3lRCu92JF9a znQpM3ERw$5Z-N&T6qK5wYHrRf8<~X8_oC&?62-B#wN1a_@m9T0IZvPtxd{m)BcY3n z%YnE{_$+}z;rpKR$LB|>;~(vbyw}DvckCI5M}nkxadKwCwZ8g?I{cEHO^O-)4}+AJya zQB60u=%PMTmJWDjXmGZM;}S!T2#t=$s|!3N`|;z)z}Q$+Q4x2s{s(+!X6A!~gRO69 z-MN+FfLB(lGzSc-<4u_-^#m2RYUZwd!fp6g-k(F z(QRht*2BDlOIY8eC38%00%+)7-xNqeNl4T8Gs)gm`ALuUp&`Y>2Q&VPL zE}=>f|Nj1p_n?Ntv0t_Lp-O<8#_}1Z3Gevq!M1hSB8v_-foW9NLlLK`I@&ug_}Y74 zOWbSziev7*7WqiLesp-gN;jB%R4+}vys#L=y&L`Gp@Ng6ph zxr>2Z>{Nb!ey*PGVLqR((@1(bmnZf2bv2dfvHH4m(FyzHJbm$(?pIWN*m;>S{=$t~ zJ81>3qz+iBd)4kX=nfV#e0jw&(j;bim6W%dgN-2}G-(y>zr7l*er(4rQ%RDE(+3_l zi{N6#C|#81q876sW(v0xXnI9Jx=b%~$)M!dlk9Bfu<-EY*RS6#ZbnAzZp|uA*81G# zwqBp8B#sd@&l!;jqL7l46~`&?c2*8HV>)?NDd3iM*}OuPyL-s6-rp;x6Qv_zhKb8C z5m2?azt)jJj~gdqv)#JZUbuZo&B0X3BF}u>s!;B@aJSH?g6GT0iD>DlSKE(IcET=G zGP)m(C?Eg)eCv~KFNGO(y+Ns2Rp2RS@MMq)W`6qY;F%h`+MU}fjsD{5o@gR0$F~Wp zYYDEC^&ZBv!qE{;yq=^fUmwz3ccO_TVvLPstg+k<{iM$Afd!dBX{?#8%;KNh@7ueB zWtNH=l5Psz)+x5UdyW0o%rd@DY`R}GPfs&LzUuf@>k}tCIXN}0&jV_*YL>`haHPY&Lz7$lC^rB?>`*RnpIx- zS>YHp-$_|Jgj%;eD{WN0apQ($@QGw3A$??XIPUyRb9h%z&q=@A(D1O^`IM>deqm8@ zA%4-?19JH6u@X~qDSYknvH|5s-=1?DRys3YTsnz(aVD$hFJMi6Ki-%r(#Tv)WvMT7 zd(1qfXg84ky-s#|%-QlF8=rXtgRK8Aa6L&Ig%?u1LDWnadP17}^KSKj*u-8TrY5(6iw{^;Q=A^#H6cn6I zkFA-$n|Vz-U-od8&vRu6hmcO}UchA?{`>duFaPS2y$~Yv%zJj)kkoH`4oAPly&J5IYmT7rVgh=&Qg+-FNlhY=AFBsZz#wgrs_Jt^R7@K7ahuF?Je!Gp)Im z^2|q~s)*T4He|oK^n=tXwG$&locL75bieg}lgsu@^CbWj%Cja)s~xu_3wTW|9X3-%Nnw}dZ1|I5A6eHtLp*H#7LZqfAQf~#j> zr6C8|mAd-+?Q2K2F(2Q{1>`Gro=?m#zb@l5edSo4GHQv5YsVWbSp%n|tgiM|i)KxV=8$-^|8F$vpwR zlo1?_F}*Yql=AWoGxCeuBb31_mp<1EfWC1g7jQY^a&l+59bH3~xbF zb6XF?o^fTs(jI5#Y5Bt*5}7K}nEtvy6pk7O|LCxuKow4KkqIC8rF;aYR5g@d zJwXdxgd^&knvPdc9NYg*^RDJCKh=VNW|h|-<6w2EX=>Y>MDaLFdap^o|6mtbRz-l1 zFXx+~g!0?GUQu0rgGaaUUfyj}s|Kl2>~oW|k+wE$(;Cmv#>Pg4B!5C$vgp1{>G`ug zJl%I3lZinN++V9)EvQuzX__0Q^HL!TKh4Y}#1axdKcJgX-u^nVhaa3_Z&XY%Co1|h zCFRncJ9l*78ShKZct@656O&1tmA7w7QL9 zSsm1^n)aDLhCalksf}r!rMItbv+=aUYvMLV)r(rS_a@b3x7bwYmUw+)>uJ%&C*Jmg zFA^DJ^YZe@dc!z>L6Hi5^M)k?jmFrXs?f+dk~*z3s>?X*t4IZ#2@w={pM`O1zP8$V ztGUU4`%c~mU$g+(2jfbWTfuT?lHO{|*U-VzW%LaVUWSXhPEbQzj`sezogBEax65`E z?rWA=Zs^JcnCuc-o^dbep}b4YVoA-#Te4OES&tO!NqQWKb!RA{4VFARE@X4&Kg+A+Da{BrCO3=}@2Ft61uO(cz=9A>D)@!`fwA2+6`WtVCmrA|)SU{ao z9DMnbV(Y)ermBf%kn+R> z@Yw_?U@ZUm#I-tBngknW*?0j(X@QCBDY{=U@X_WDwnlc6 zzO4Vd(-Xi5qDS7UKf8UEe%0U*5D-+@u^gpCuCs*Jc>JRc!IJmWWmlbyE+}&s0jagU zK!19AuvYZ5Kg4RnR>|~`oW7;Sa52vfuqv*iqT<>_<=y3LO6r{o)EMPBDvZgGkA`A% z0T1666s^|{8VJrW;d~OZA7Bx49MyQd#KBXnldnGer?jyHqZsw{em$bB&UR?=?{ zTV8_e!@vh)iM+RO=e`%+S5Rfup`)tlf>TD_d*^6iKrKU9D- z+8%S={&R4AJb$>eROP#!GoT&)Jggt{CmpzywW)e~UW3TFp?rBu2OC0zyMk3U0ho_= zn$O=^5G%5=buEbeerN-!_t=*rh(Nf~1+}Opw!61Gv$>hn)O7RU@UXn=_PYdTd8U<> z6~k(Gj&lJdMg1uQP&(ffdZ~(zDe;qCKT4YSPyN5;m6hi8PpgIVf5HJ{<^=rPxMon2 zf#neMTu?-WY`!z8^W<=+x>wLamP(41mGuIkmySa1+g3>D=6$g_YGsdU|?55JjfpmkBLgT#^{2z44%%Q8HYPsH&2b zym7-#hh&8vkhSOL6eG2uS=(^I>h$>&wFmGA8M}tagrS8k)iI(wk8dws@ zV#x}I%TgQbSAF0&Qh?4sJB|@W5i_g!*R1+nW7oM(Rg)MO7Z)BHdVxp({WZ8(v)9HX zEe;OOq*3Z;G6K`i_KuF0=H?4f;vf*oGT@=04?qVU@3Trs7(h6+wHO>Iix-jPTw2JBLmMZRC^{K4r3Wi7_UJoat@rxeXmOq%{G5-C?NX_Eqb-K~ zG_J8x-mK0y6i684sxJ8L8a%X}on2@OiNgyXz2YXM$a}A0{VPR1h6uJSFE4+p#_sCs zN&+w0u`-y8i!^FmTdoLxy8_HddwY9xUmsCNM+ZFkkffQJ*}>YU{<5HB@Bi}`ZVC*8 zowwP^ka%9~eO(oF{AIu``ivYd2Hs5WL-6Sl5QD$rX({1(c(}N51thiX)`qoXZdVx7c5`#PcK_|w|Kd;~Z@b}Wl^57MH%Gl8dp=Lb_A72m%67K4k^Act z2z#V<=MK3P(I75FgszhlpGze+f361c(Wi90#jTdzF6esvJ94$+ARQR zj(#wg%I|YXjhcmf`$9RnO=1%&}ViS-}hKVpRo?1H1&0zjSLRT1!{P+uPfI zDDR5*`cHIN*hL7l`fzkfI1=QRmKPiy9pP3Ej3FT*?3|o)aMWOp{c+*W$f^$xl9I(J zLB2LBfIPglv(uW$D9w`cH6l?_HyS?Vpo>oX>+bU4rMp=&&8fTw5s3=O&#fPd&}PX6 zeN*7#xp4RH-KyGJk725zYp@7A(8WW058SN!%=&1twzhV3Y%G}+5h)f`P2VithSIlf zNUp6ve!MhT$rFaxzbn}W{s zG4fR16i!VGYwOkzdbioO3ThH(ulk@CgoT7|0^i}bzxDvwKAmOBym|O+C4sb012NCv zYreiNBq7lWgd;jpK`IelqNsEKesWe;p8}T-3L1RR)$dneXIcR@z>7=8!yi;dN+&<^ zTpqYU7K05PHMJD}iV!S$3>?6Q205tM@JZ}EFJ8Q`AAUyp4RnSr(4+uqqx1gHj4S42r*stQ{RBzkfakX=BpwdblPA!`lSAUoYe1Xkj|mYXa{cj>C>kyo(w}5 zVAYybN24PKi}iR74Gk?|ACa|cg4zcMuI$T~$o%|#s9v72F)>%9yjCM1U$Jm-C@)L0 zyTi34^z_~DJW!#ZU8b2J^+7V!g&n>m*_x>)Nkv74aJa5pz}VW_;xk4m#iIq>0b^}% zZ-ZQP!3&5Ugj4h1EgaBRYlG6C_Vno!*b#V#Cr(aI19g5LK%L_Pn1ala47h{=mf9iG z`B%;C-k&i^kHvVnE)TAwrl!UP_U1Xiu|Dj(RPWcXSKtpip&|Ii$}SNJyT1z({s4_#W+Y6J)A| zBiV1NKAxNr!_#(1b9Vrk{&!csmP@hMz) zQ8#WpfV~2JF4=!)kr0mFgJmVU_Yehe_qK+hV@N8X(NS}kz+)b&r}Cgn8_(#Xh!~;x z=vu*5v2f@hjsMS~Q_3FrKadZj2ywy>K(hz}8J_$C)LZyT&fROqU-*nZ+lK;@aCg+> zA*HHOgWBkhmPGLn{y)$fe+S%F?xRYcEaaE1c4~9i|M_z~8#}vsfM*aR{BwYLmin{j z=i1|t+qElDv-Y`i4aotX*xKJmmb$uv2MW7@f(yZ~!9ndAb|qbgv$HebgDqAR8yhUG zQ8fG#3FY$g(3dY?!u}W1fH;*CHPMZ$&f z4C^Bx_KZ(Vc)-&E4Cq1tFH{BO>GFX9gS>`39YjgdfSXzzYyl^bcp@+3u9IHqe_; zIkc|?p6-naUBBJ|k%zoY7+OFCau_=iFo*VIC5%MOa`C&no{&?hARw+noXQ{@50%LI zgSv8y0ySxoZY(4KIQ>d3@E{+IDq3LEpT2(03paFy2=_bu%K^Cq#9Gu4vOW3vv%n5B z2PJ|22Mic=1w|bX^AKR(qWK`Yt-z{1*8Y1TDb%~YsnKFM%>`PS3O8?Jz17N$U0ZVs z)p^DVPof4@Lqk*3(%PC0u#yf6vRj?+_C;y|6D%yKP!^k0^~=9%C8+oep8%nYNB>kT zEOg+4njhrQndN1?7$p>PSZ<6e-^`7Cr1K5hup#-nvD$8)}{m} zp9((vrl;p;*aSs5KnenBY*5@F4Il@I`!-+zME3Gn>chNwGb2CUU+Yl=>;c)=uV1^q zeVdDrM3Dlnx&FmvIZ}!WzyNWCyoEKMg0wDEiZ+G78RU zWMpLFJxmxxmD;WS{r#=2t$PQ)W|%(^C~9p`;-{vNvjUkbIhLB{3?Uv?C%S-rNemtn4pOU|I=p{RB>bTle?x6wutZN=ygB;{UhF(f@OZ;he$4EAg-- zs||p>gpG^a+SRq3vdQ)f4pt~6tMMN{n&E$tMDrA&ZnuL1aqq1LEtIs&kbbea+DKOt zS4ZAix5rX~ZWjxAuoYfn20|E)1EZC-b@F!F)I-<( zUxcEUV0!w?TX)Tw538%yf|H+stk(kP^#ayNaW$*Z7VhrKFs{D7eoOHEJir();t~>Y zkb>x$3KE!=6>8&GV-nIRVBGa~Y2^t)f{Ou0I@^RuN_IhbS<2KiJ#c8_16nT4F0LE>^p`qiIK7xVt5}ji~xXquO zgV4^|1$F9eE2hS7zQac8uU}fNzY@2fZm7rCNhyZ*{{>Q>etix(53UOEotZ>W3)y}( z!Y)0d6_;~)vt>6sdi@Kc53)w}HP4FPMSv5+DgxX4EG4BF7L5V&!oT;%l|+zlR8oI> zZ4F~Q3dfEd^Ak+p)-i?|Jnk6qyHjSjDfij_2N_VP>h#&VQRtF(e0+QoX6Al?)>02o zkM{8J@NT|yIkH&$xOdI;?fapAR*0j#`#u`K$`~tiat47Gdie0+wNsfq6FyIVDrRQl zBHeGi5EyQdSzAQTgTFB>uN3X8`c@dprPl@1J1eVVj4L;0p7Zv0D2!Qfb8|;0Bq-my z)r{NN2f4$y7O8RG!^x3;${e^@HWnYDJLD*?GZIw|RVW{jQe%C}TrTx#mHmX9IFNdSjl zrH~~TgF;6EL`^%a9I^(tL>}7YS$iJaZn>8x-Y?L?e-q{u~?NJ_p!oyZ{SO37DNQKK3Uh39R>OwEbXlznKZrO4#BXVrz_}q@W;#eLBUl zB(Ti!jM7wqGI`XLl~zB3L*jXPIokZwXuI6?MJ*x0^9;T-SuTK}19u%u+9#hGFW{87&BG#3jC zd&9MP(cz(ceI=*{M%wro$1fJ+C;4UNK20CpTFU}P$j~C#e*^z}_L> zUXEKAiS&wfwcSuCohXn_ z#&1Y9w|uKnng@%38Ug;+qL)bGdCirvbIBwv&_jJ?43g-oy#8=P5mkR4_dKq0vLYfo z+kEz$D>EWjxHKH9XvRdVjaXS(-D}QwkLi3M!A8U7<8%RwhK3Gvp7+51M{Zin{Eu4w zTV`fVS@I!$%C=%q1MUFPWBG667xQUNaxy6*#RD56{uYJO`}VCbvb^X)Oy`P>JVEr^Z*wj=@kn7|1vWTc#u}#+Ax@C?GO(|_9`k$iuA_~}gP2drf44+E`7Q13 zqk(1&*>V(j994p<|2-o*TDSLm8@3-$6@H0PE>#{5j_;wEK4yN${k*TuOF)c(z%(w< zenb+mc|5JC$`)l_wAvvc$!(sqqM^=&n!jATySw+M1|MMN&E*mhnCbNRacV}qdKfHX zR}_;uy^OCAs6yCkib}EEV;tv{VcPtWY!V&W{pi}~(-Q;I%Hk7Lf&Lw64< z%~pjmk>KFq=$M#LP_R@I84wOthn1L^n1-Hyext$_jbHDO#$^99KT1c`#_l0V8PYUI zK_U5xcQ`ux2P%3dCN^55h*e*T*kEhHTS5YjK$Mfoto4ECnI$38b<{&)7q!|(Zq_M` z2~}5DSNZKgIHM3P?U8*>h^2bJmZaZbc4!7z;Xcd+rWUwF3{5=`K9SIphW;f=w4eIG_2~QY#Xjc| z%ZDekK?61-v}SFx&8PIql8rvN&Zm30PEL=7*@Z}er!=rX_e2euSXx>hK%o~78rEtq z86x<*BV67&CX|XL;cOC>#Ux)oBF5(#3eCFZa+&3Ld5|=yLD668adY%rIq~^_9qrBr zWsHBJE%14ae)o>|`s2xHV0TS$iel98fQ#3BE#8`mo3u3?Dk{XUr@l^$Xds#0)U%)$ zZ0+so9UrbVZX57jV2}%>*xAousMT^;~l&7?> z5NJjtA1ni9^m#@`bi|v2f~W-g>nv=0;$Qf(a`$&Sr)tAZc|%&N+~y4z>EaERA3@I? z`Q!FIzSJ z{&7%T;Lk*VCwQA7rST88u_JFpl=(tGIU$Ja;{L}s3<=K7Xd2y9hPoNBklcR5f~G^j zJc&&;@r`4pb60=A!I59IT2P)2xARopl^7*Zl3uYm?LW*IKl}8p|26D9?dgf7)te~m z&cu&B;tZt&Bfr5zw>*Hk^vY*52K+PMKkoa3TR~X9Oz>!TBcV*l(yo(CdmC8fd4P8MPMhN^#8ktg*0@>AUuFiNSlar~>Smgz&rC$hkgqGd=4$Wlf>9M7Y zOX>6S+ZBe(kG6U%r{}r`t?_Pn5{-UY81Z+mHc31T4I=S5rgBzQ^;qpM2Rd=PQ1TubnnU5PC;4F*Ce&* zgk}Jq!p@VDG&HWK!obuTl!LzW23D72>MDlatT8Y&dHvNJ>tkrw^NRE2 zmE&e*f@IcG#MsEP((WNb47!t9zpV+oV^tYJQqKQS&APwFz9*c;WFS_|pH5Qkq%Zr%c|%5?BC|HVs}X4ltE zpSh%Bw<#E4AQiRr_Y=cbW3+}}gxP-W?Q3aLnWDMQPO`JuOD;x`Tmr77Hqa@y;Q7#< zcvr4)59xh^jurS9goA^If<;E=;=j%5a9tk$z@*d)aS%`j>H~blT&5T_Uy1o`e#`z$ zOkj!z4KFH}>#lWs(VccPvBlAaxP$c>rpbD$#NX8Ymd@IAsj~G0{H_!{0sh?2?dIzo zsyrVugZel>&!zO}MMg$E=zGxF=8SjN)YMq@z7ogJHWoKKa^`#WHPbJX(xoGh#<3NRM>?~_yD8|#rYi8i zy{K%36&xtHRz11VDn7Nm;66a*_esLOp*ffY#97atE1^5tR<;u>)lF z_xE=_)F`)8JU3m$0xum=)VGnR8Cxc^CaIH-uo()o^D%`FCnqO`0RE!niAHmj3DRG^ z;*pK|^hvaIKsgd!f&zQF>w6u-j<>ez^rE7&gh?*bVb*#Q=+45mS#)$4p6e=hvTI;a zkg!!-^m*W4WmNL>1MlNbAmG@5a{-=^hc^3r5_}Lblbn+BSg|^z$7xSYS8ty}+ATH; zrq%{`y*e!|l}C??|AvujmxHc811Gpp@7*ZZv_Y-+V@WBgV)pR3I09hU=jP}2MC6K~ zYAJvG^wBb>ZQ|j?3xOt=sQsMHh!y;PcU-KL^IyVUU%zVWi=lWTBOlL*+WJx(4UJrE*Fzn(=bvql`zYhe0T2h$i(%S~IU9O+`|WgM)MT0D z0;6NdxmtR9u7bvT3yn`Xv$`4!034iatC|0Pj*q_qGYn6+m4ic!h|M>a>(}*dmRR=Y zC~IN??xW--D%zOmT`{o-Z+POdQNrZSClIThn{eDv?;%ki7$^S{eM?TLNEOQm>va#WPHlCry2J?_P_N;M_P2owl5zklGG40 zet2$DN;YR{WfeA*e}~sb26f~D#7h%UMhJTf#NTqg-QeEdp0Ld~3f)pO#-`~%Uqbdq z40!s1VxXp~i3RKL%=Y>K9Uo=Y#>&Pqc^p&ytZ*tBC|A<;1SABSLkBc|gZX!; zv9YmlZptr?7#bN_ot+$lesup%N2TbmY}tUz1_lNd87W9hjB@eetMQRF!6;-`+a!Ht zW1ZLJ$&&TQV$R-HXMP=hmJKFuaCf`zE^8wO^I+lu2Bf^aykgEn*wp*$6jJJ#9bE5LP%ikhdr4P{K_~9XxIx~T#M}w;#UNy^Ta{X8 z6krj67X}^?pYY1nw)JRUU-!)~1KPldxVcZ*BGySD{(&AQ%Li6aBVFhM94WAca~gf2 z@SgacGecH32c-rBC9|nS0Vm_Y>(hX?>(0<-bdp~L+#4vz7#$rQGL-~6H1I%thGke@ ze?}>#bdp`)*7}wW=ygH8X^kXm|4~1IQ?D-A)!+H{a9F$Ik+_)sz|ArSB`ZphU`W|i zSVTq1K~8q{F>t!i$qE%@1}M{v6#?L(Dd{2VCX1^rZCpw4|L)*?E%WIWkqPi+9v#)E z5w*Pv*c|)oEp`>3jmc&Z@+?8qgNj3?B{ode*Vl&@{dK#xd_b=Ulv~(6g|&L=aj1Vk z`_F$niy?3g60IdDieSuWhqGPozMu|}x=@xlIo+h%9S33Ga&y^&k5;(G$H(y)i^i+| zd$EzG&Q9Fs=H~HgD`D_O6@S^`Z1hcJj@pe1@bb~flA&#AXfXU_OL+8eGYP;m=I-9!HW){MIg+I!I%eXkbO|jE zPK&K3M}M963d>_5w(HM0oC*F0>(dG+wj4M|fM?bK;tXp%i+<`CqJ~re21EJnfSYZv zS58}fFVr?NU&LZIB`2V&p`)dvv+CSm2O_BH)ylju*Bh>9>Q$lL@(W7~mYsi=EQXP~ag@iU|&D0FX5^{~Ag#`-SW{Re8_gUN}g^0s*M63dg`pACIP`WFwlh0 zn4snme17VKKq;_qrC)zc_n*5Uj|y2jsBdl{T|ygAz!*i*^k75xAz&Q)Up3;_oyMQ& ze=xEHN|Z*-p5#BBNhOa0M6(vCP1=pYK|NWr#&+Kr4vq)K`#i^-`8;{9!@FFi6~TU| z{~;_jl@ge#q?e+0D8%1juY?a&4AQ57l!y2itmoQdgutC6^}-1B(F<52czJB?|dO+Q^;=uYc|;6Tc}OvsrDT+WQdGdvrTWXe0}|KL?XK+ zXtPu`RMwajog329^PS8gZP5K~!IVSb`RTv$&kp3U-4Tf@y4L_Xft;Z|%WLp(% zX6DOkbj?40&|bN61vU;03fwSjsLlnPGiarV2@)Eg@oEpQ%U5ZhfG}!mBi%zvIRY<0 zLO~G$edfvT@ZGK5T?(mZ9j@NqktPC58a|*Pa-|B3iz_zNPk=)3~&yP;bD+f^Khc3xCT+Y)+Oif@cY-wrP`eB!C3Fr5JmrPhf zR0Z_ZJ=yZikaSz2y9AJi@?=j~Tw99=i56r1^rGQ1JD#+(v_`fZBM4#bK(7n?ZdpV` zMnVWHF59ys=B_`ZA6lVbhZrCR&h9e&Q|KJ>g{4qaQ=3WQ202wLtoVKb5txhP;OXZ9 zoh*-)GHYeYfccWsQ*6iz8Xsf;1Uy+W%)tl1HeMZA!Lg2Q{rWWqvN{DpCN?crtSHU+1 z4d)e*%s}9vio8l22Iv6A&#wuWatsX(2|-UiS?5QNj8Q-s!uokh4x0v)58R$dw0$^# zEr1IUugdP9F=Ew3hEQD|N+~*rGLR9Ww9dB2QNvIHDRdcdX+;(Gdv7aDii!wWTwoNZ z+-+Xv`v+rc0O!qRmaVSG`;TGcFM~G%87NH7&Q1dT3!Kqc7;nFGOQBkCQ{YO_2LUaf zUV;d&355Tw#`^0sj{j_0&qh7Gy*nUF!*hd1c$?SP z*Vh6RH)LYx>|mZ4d2VpUEIsyR{I`@HVs&z`U1(A*f?%5e91v%tS z4-IgrO1CwK;SQ{)gx1|A57g@p}#vb$Sy_Eups zPx%-YjskF)kmvF}2=E+$#t0Q>+m{go8x5C|A@y%4?>0hAA=))49r%p5cc=P1`w{yW zJ|!tZrqaT?)6x}kP8KOCy3^x-op9EZ)6%#+#dHRcv*W*O4>60iv(&E&Js%)Ga&QM> zJ%J#{gKNR2^X9=PTf}1w&>)K6_>09%Q|JMxC2&^TDVUiNYcPZX3#1A#-9f6Lb+N1Y zG*E#H7k5La4WHTaO5D-H(J>Ahd25&=gCj^HMMnwKXT(UF0zCjpQ-HL{q@HOZSq}>cfyrbRIEUTD*N=Q^%=(<3p+b5Pxj2V zmm5FPXf3H*@YLG5DtH70im*vQI1nLu8t^IVw|n)#_A+x{wtNy`?B?NNK!kiZ!K(o0 z5t8>dS4q=SP^ z4W>(WTwL6qm!i0$wmmmN>%VyMVp;I1|Je*#G@t@QLmHUrih&6!D=7IwFo7iXqHkWq zXB~v_Ob$q?P;=j#H!>ky5){xVBr$;!TSr)`0zgRqbe9|v_JLSMy1ctd@~^}~Q&PyW zN>)nv56lTyWPnfIT^%6+h~Vns(F#`zd#@R`wYA|fYVJaPg4r=o=)my-`Ky9$Ga^yx z+ydYUq*73fVdnibDKnGt?cMCeYL6upH}@6TpAL}8{&Q4|X{+D4(|WKykGRHrUWpTc zWtCcrEa(Y50JG^Zcs_DMpndo+pMsHc7EZ~3`7~1s=f8Y9sHUOuAJet=L-fCXZ1p=S zTpj@|Md?5VHkc?c#mC1FK)OcKDF`HOVK@XrNIDHXxVLO?BnU2uTeoh3r?CTe`yMpu z7`8z!NvOq8!ZU=5almSj4VJ!eq;Z;ULBn8N+f!D>GQbny1-Zhg$@{Azh#mkq%z@o_ zAz+HHs;UZ~5CW~_&u}n6Vj~$G3WhM)$}?a9dCR*FNvN+zs{#*25Y-9*0Af=BSfHSq zgYuOg&dknrl%tZ;CZ(mJc>{H5yvmJjb@W3Vq|U3%atzS-kxISlkcLr9K=7B%&CL;f z1|B#vDKbh@h!$eFRydXW{$*(96xG!5fU}MyW=@2}%_c6chuKbKmKXTBZo1%p1KD)= zaV}s~Bau!Sh$if4oZ2o>?t3OM&6&m?6 zza3jjPRd9aX@(6Y0d^jYW57O<{^!i1;(p>-fkFZOFyb{ulKRo^$|c0{!l&~Tc9o2rJUKO$!;|6& z8;o#dh7TTC{_aXPYzVvo`z!>w3@PzoTVa-6Op-sl>FX=`q_NZ%NSJG}{HC8ieiZWi z`_QD$mlVujD$q7UuyP=WI~1*zrv$bs==8XeiUST{q>#X0D1hQFy@?D`z&1|%@dK3x z2Vph-ohK;r2)x*ss*i)QA`IjOFmZFZy1R!dp#ad(CBkR^2a3UldEI$38t|*t{|$-> zrays+7|7ANdTU|^$tTbp6pEG%v9q%yGCaJ2VV$oqbQ0X2>^kt$< zkrhOtNp`$%HVSPaQjTr$r?t5w9K`<-lWs_4jspr9eIK z@<^}-1Oy;CY;<%Ke~GiV0uoq;YycJD>vsd%%WQDc9(iuhDfgrc=c;XHO1Llq%7_yW zHZr0MK3t%Ff0eKurtJ-l9^^h#B}8Tdp4D>kedum#8X&ST3L3ZOJ1}Ce>+$ zM}K*BOLTBTcY~eEiv6aAWDg+P;1Yb&CL5C!0ku0GWAPM-t21k(@ z3I3G2IvIqd>d%%J96EmNztmqt80&e* z6o%lZDqIl_4WLCR;GsL>Cno3A zuz2|g3sRLDJDFDeum#wY#F0b_RX#Ro63VF?6t1M;Ai#86Fu~T!Ri0k%CvjOTGwS2Q zo$IV0Jb$<^i`$AUu1m(}Nvt4NL$F+6VL|jD&<~C^;x+(QqiQ_6I>lQ=SU40a0HkvZ z=7uyCCG`s1UOF^&!h??wugJ*ASU5Nw_Wc3Gfb%Aul_bUt?Vl4oKV@8ee49(->D#Ss7zXG7NmAHh zD6eJgi}SBoxcKwhAU$G|`S|W342Q$0Gj+bYF~TjtKq*qqfgaFvmm6{dB>$gtR(P4s zw=`Z24cQYi;Db3gDD373cWx;uVZ*m4#C0&%7JcrS28-2!ZJXrhKhk)EgM&~TbXN}5 zJdjQf7^)NLHyFw8Otp5HiP}EebF2$KrGsOwcKbH>N#k_G8T=@(pdiFO13|Zd^6aPT za6Z~J)n%$$yy^IA|NkC-%6u)>3fL3CA{wS)2i%s;eUXocn6-p~NmbR(yA5D$A%?V+ z&37snbTqS3Uu#%)oNm<3a1d~Nf_agzBb}%hQPtiXRj7Yh;+j46cq>g{x&yXpu?TQc zd`lBdf`tp&`UBEXRBf$v&E>?1Zabz&TK^=ZRqX;L{89S-uOgrhH-qiwDFgde$V-SV zmwaA~LSB(;hzvY7Wn~{#3PKgJdk0rv%awU}B3psed#hze!>1S8%b|;4GaavTAb;h3 zcsWv~gM(>vx8%d}-v`QpOQ!kbovSM=7@MRiq;mx>UvG{t4{-XcI7p>9h}&lOv)JdY zO})8t7@e}%343tO+Oc4ZMpOFu5?RdX=CoG((%f9j%MChcCi8HJ7-aC^U|KkC-kU2Q z)u1Q#nXcI?o>rR`=6m1WRNzJc2ePouPpuZ}{Uxl@A#oB&dIU-ls&9%7*2$7QWFg2Cw( zgA z&k|i40pVdMGFUflf6~Kb6}Q-zk+I3od_nYwL%Qs0t>1j7d@z1?Q&D!IxdS|-wfW|3;Lr!vp>?E{&}C;&?t?2zY2Qp z8xI}`iHq~^m*@Qg#}3#8I>GA!N1o8&gONB^({R*3?R=FFR?IFBoh~9isDRVShRlKC zvE!Ms*g5!4lHUu9P4E=}nEBfQw6wIRORqLSu7ks>RPDa-JzWEwp)q0^X=&6~uU_4{ zcJx*OUBZ);<8l!zFfb5XOF;oJnXI%lJ#2yR-`}@q&!N!+Vv000G<12qJa<6b>oP=5 z>&x`?H#N1mOy{LHQ*FjiIB2ei#I$tECpKImxO0c$hCBAu)Re1_PZZ`=r6wsd^oFFgo226emoz1PrW27Ej_{dBbFjEKtg^0xPl|?{!CB(NgmyNalR9?icn;+NV2B9w1HtR{lShqdhNaB%CKh8swqDfYwYA5;f2^ zUJFn4Us)~@St#Z0f+qE z$MkZWQkLxe?gjTA;;gq8}S6c_IN9u<`KV zLuFC+hrvrP-Hd3t2pHKivIXOXVXd?5;-(aSLGh~bcMjn*+79nWp+xHq9nfXwJoAA5 zy3Qj&4wej@+CT@&vsm)MMmR2?HQ>uZ zAiz?_%q353+Ifkw-8K(R}(Sz`* z-P*59NJ#khXwnnMW7+Ob?+X@Ly;f2jdIXj1RgG= zJNO zG_X88tmTj3=(KfqQolF`Lg#5}Dl+tpte9(9Cj1-FR<;Dv4tVW90i}OdRxF_nyQWwC z9IFUu$GinV>xgNBRWT~(CZ}q*i+`@ozpQJH1}7J_U}uJFL)v4^C0GTe30UP%5yr#* zm*^a@kyS8NKKr|-)@M_JGX*U94O^@Az_7wp8nslh{xkJ_~TDZf(HxdYCm0%_gbe;+Ua9B(t{N}Bsylq)1A zbQ2kS1;Rk`Mp1A}rWB};$e=hH=ltZ*1Ez5?{(GzFxCtN$xBpi=SK<$4{>Mix)~JMt z3Z*r%s5&pbZ#aMDU@Tz5u=#- zy`TPz-|O}J2j+R6d7kg*cwbXAQ@BC2fo4FuKrj0O{0l)}OH9&XJ5Y|>iNZ;&1?Ml6 zTDiV3BAuB>Js&^r4WoNLq=mEk$X5F4?}4z;rK5mE@?x-H9nYUzF>~~$Z5Dic<%&QrU$X?v|0+-p#n{Ocyze0q?9aP1U}ca zOw4L{$(8@8am6w$LmPUu;Auf`LrH+BPiXlz>E65tWEGRJj+H>-FEAJB1Coq&aD2DR zind<}LJ{(!3mF;iykhZISruDc3?=(M zmvdhf@Rg7rD)XY6YwFCw*)W}ft^x#cdR+7v#RCoz1^SjIV76$hDFU39f{%WOlr%I**`0V zmeKtIg^I>nv}&AB$f3l>zF3I^2#+d`22@<6v&F?j=2SNDeS-~Rf_y`D=WJaL#u@Mj ziU^%^2}^!w#a;uoto^=^Y8TV`M+$@Fy?pbZ;Ph_Na0Y0B;VZZWzm{5*$!kWOW@>oy9YQ3m05 zMksGQ^gcrd{&-p0u?QSQMfe2I|U)X9r}w(EPM@eN}jC(t)4e zsH>~JWT{&gzmK07c=~CWOp7N@($Vt4!6P7IREE^xU09%DwgjCS(MHe+`0}25dqlq9 zok_mev!ju|KGVE#DLkGrKpEvPTJwTlNhzkN0?_g-XM*(gxO-e3?h8!vq}jTW(f*t$ zyUJ0%oM%6&2PrWtyo!Af9VxuFr797|GsKk!BOH#b^&pUM34qEM46Nv5hEH;8cH+fu zsw(wLAzR#7W$C?DbSIpr!16T8IhaT=krnoVv$ehysP>cahpC#U8WRQQ=uI$!MBBFU z_j*WE2mC4;YE=Ms}Wn0|lcCyNsg?>)*vF`Ur16MN-xy8A3 zT8=p9JGwz`qgatO0x(lyhGfAc4uYs2!l#bFzoX*DU4u$phl|IUrq#D?(wE(d-$2a~ z6Zw+Q_Sn{RpYZi1$;!rX#!I1h_wx4k3kcXRx{5-Z3vFV$MXjrP^}k*g$5zg^1+)B* zEJQwQ%EkED*sy_ih-L*#LIJqKi6_dTGVN__w(;vpD2O}Ke(%O+b#zj}WRs~aTs(uS zYUiOZRnaU%e8q~l_hcL$9ksl5m|`vB3FHqQI>bUT!WQB07jAIB{2zF$T{>%&7z`EO zNLw3c-XGK`1qF7*?c4h2mX@3TeA`du#?)whTyZsq_GWV8&?LK^5Bk5wYVADY22}wx za6zD@t#biD0l2TpUHR4aphzle^U$#B+z}0bCG2bdL z-k5(mBTK$VK%3+f=f~_RFhZzSw_P&$>-SnmThYw(Lf3BV-u8YUHq9lIDP9hUK>JxR zHWdfwT~^}>1G?@F5C3h6VJxz#iJvNb@1l%Un&idH&z)QD-se1SupoPBZcu4U=Vn6e zuz53tjvYE^liC^sLqqZ;q<{R%vI-3;PTI=UX#8hne{yPi+@AcBU%FcqR;wD?Ws4u4B9IK$^vcA4J z3w8xkeXJrkxR1#vG&|c*&EEfD50KzGoKeyJ9h-<}#x@GQ!I7?8>TbK$6K*k1Z(rxd z4%38EzbTh4?*hf^_H{=L;=C>y$Sqb(af45f9#gR9^PNv;wGJD(4%JkyduCeX{p*V$ z$%2C2*?Yq1TfCd#J=4|)+|OuAQ0)Z>g8-(4uBTZ!88JH7=clBLW&DO-SGK9t51@(- zYDh^*AufT_s;=yNO^v#|wx6acW=VM%-^ijE!~!+Efax zXDSWb9jMuikU5!~zcq(P93vB{O@nN4J*v}8rl%a>r`SOYpeaNQWa~wv1Znxhy1NX<`9OJiuR zZf1p73w#eAz99@-o73diQbC7dOQEZ3hK%|_uRAmLI=LW}D9eLe^9u_6fi)8sZFAb! z^Ej!y<8#)#pr8`ZyuSQBdZj-9VJh=Btsdv7r`@gK{GGfQ9V<(@C+$2D>1;cNIp)^B zN|~WoW%eicDCz%Pa}BVx>y$9MGE272DLoo7k>e95Q!b~S#}|wwJ(+{Kfvb!*N@JvJ z#=1{XQkg*J=SE`5s0Oj|MrikN@;1VE6DJh@XmcRJ#_e!FRZ#p3OPe+%;=FE=#oQK+ z5Yw_pd%xd<4kg`bZ1U5uplY?ZZq=etC`1AXm}Pp>jrGKdM7(tRdpNuPpF;gV8p17% ZI+pyvSUy8X55eXnN84RCrB=St{{^I4rYisd literal 19017 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 aJ^9s}$EoRX8h+LZrJ Date: Sun, 12 Dec 2021 21:14:21 +1100 Subject: [PATCH 0552/1681] fix: Aspect ratio in matplotlib plotting --- src/igraph/drawing/matplotlib/graph.py | 4 ++++ .../test_graph/clustering_directed.png | Bin 36485 -> 35633 bytes .../test_graph/clustering_directed_large.png | Bin 67108 -> 60787 bytes .../test_graph/graph_basic.png | Bin 25084 -> 24565 bytes .../test_graph/graph_directed.png | Bin 26545 -> 25533 bytes .../test_graph/graph_edit_children.png | Bin 24552 -> 24177 bytes .../test_graph/graph_mark_groups_directed.png | Bin 26545 -> 25533 bytes 7 files changed, 4 insertions(+) diff --git a/src/igraph/drawing/matplotlib/graph.py b/src/igraph/drawing/matplotlib/graph.py index 3c7742efc..28c2eb8a4 100644 --- a/src/igraph/drawing/matplotlib/graph.py +++ b/src/igraph/drawing/matplotlib/graph.py @@ -316,4 +316,8 @@ def draw(self, graph, *args, **kwds): ax.set_xticks([]) ax.set_yticks([]) + # Set equal aspect to get actual circles + ax.set_aspect(1) + + # Autoscale for x/y axis limits ax.autoscale_view() diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed.png b/tests/drawing/matplotlib/baseline_images/test_graph/clustering_directed.png index b8ae42ae5db3d7741064b11b0f2c36dff606091d..c2dd4cc65ced7ca3d0e2eedd132c68521ab32fab 100644 GIT binary patch literal 35633 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

  • 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 literal 26448 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 diff --git a/doc/source/tutorials/quickstart/quickstart.rst b/doc/source/tutorials/quickstart/quickstart.rst index c8a57c355..71792d7ee 100644 --- a/doc/source/tutorials/quickstart/quickstart.rst +++ b/doc/source/tutorials/quickstart/quickstart.rst @@ -46,6 +46,7 @@ This example is aimed at those with some familiarity with python and/or graphing vertex_label=g.vs["name"], vertex_label_size=7.0, edge_width=[2 if married else 1 for married in g.es["married"]], + edge_color=["#F00" if married else "#000" for married in g.es["married"]], ) plt.show() From d5cdc3fbfd2f0b1f51fc932325e72bcfb31ff974 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 30 Nov 2021 15:37:22 +1100 Subject: [PATCH 0533/1681] Fix aspect ratio of tutorials to 1.0 for the sake of beauty --- ...rtite_mathing.py => bipartite_matching.py} | 4 +++- .../bipartite_matching/bipartite_matching.rst | 3 ++- .../bipartite_matching/figures/bipartite.png | Bin 33545 -> 21819 bytes .../assets/bipartite_matching_maxflow.py | 1 + .../bipartite_matching_maxflow.rst | 1 + .../figures/bipartite_matching_maxflow.png | Bin 50120 -> 26165 bytes .../erdos_renyi/assets/erdos_renyi.py | 4 ++++ .../tutorials/erdos_renyi/erdos_renyi.rst | 4 ++++ .../erdos_renyi/figures/erdos_renyi_m.png | Bin 153627 -> 135643 bytes .../erdos_renyi/figures/erdos_renyi_p.png | Bin 100941 -> 99833 bytes .../tutorials/maxflow/assets/maxflow.py | 1 + .../tutorials/maxflow/figures/maxflow.png | Bin 27217 -> 27330 bytes doc/source/tutorials/maxflow/maxflow.rst | 2 +- .../tutorials/quickstart/assets/quickstart.py | 1 + .../quickstart/figures/social_network.png | Bin 26637 -> 28909 bytes .../tutorials/quickstart/quickstart.rst | 1 + 16 files changed, 19 insertions(+), 3 deletions(-) rename doc/source/tutorials/bipartite_matching/assets/{bipartite_mathing.py => bipartite_matching.py} (94%) diff --git a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py similarity index 94% rename from doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py rename to doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py index 65c5d83dc..c4d487f69 100644 --- a/doc/source/tutorials/bipartite_matching/assets/bipartite_mathing.py +++ b/doc/source/tutorials/bipartite_matching/assets/bipartite_matching.py @@ -3,7 +3,7 @@ # Assign nodes 0-4 to one side, and the nodes 5-8 to the other side g = ig.Graph.Bipartite( - [0,0,0,0,0,1,1,1,1], + [0, 0, 0, 0, 0, 1, 1, 1, 1], [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) assert(g.is_bipartite()) @@ -29,6 +29,8 @@ vertex_color="lightblue", edge_width=[2.5 if e.target==matching.match_of(e.source) else 1.0 for e in g.es] ) +ax.set_aspect(1) + plt.show() # Matching is: diff --git a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst index 59c636a52..45f2b7cbf 100644 --- a/doc/source/tutorials/bipartite_matching/bipartite_matching.rst +++ b/doc/source/tutorials/bipartite_matching/bipartite_matching.rst @@ -11,7 +11,7 @@ This example demonstrates an efficient way to find and visualise a maximum bipar # Assign nodes 0-4 to one side, and the nodes 5-8 to the other side g = ig.Graph.Bipartite( - [0,0,0,0,0,1,1,1,1], + [0, 0, 0, 0, 0, 1, 1, 1, 1], [(0, 5), (1, 6), (1, 7), (2, 5), (2, 8), (3, 6), (4, 5), (4, 6)] ) assert(g.is_bipartite()) @@ -48,6 +48,7 @@ And finally display the bipartite graph with matchings highlighted. vertex_color="lightblue", edge_width=[2.5 if e.target==matching.match_of(e.source) else 1.0 for e in g.es] ) + ax.set_aspect(1) plt.show() The received output is diff --git a/doc/source/tutorials/bipartite_matching/figures/bipartite.png b/doc/source/tutorials/bipartite_matching/figures/bipartite.png index 3565aba619105e82297afe4aa60406949e319645..1d14a849d820c783e05869e03bc9ed1a85d0bfd9 100644 GIT binary patch literal 21819 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 Qqhc_2*3MQ97Jf;80lOo^CIA2c literal 33545 zcmeGEWmJ~y8$F7?2vXABDT;K1bc2F|C?Oq+bVy1H(v7HqsH7;JqI8GSjUWxu9n$RU zS-*SxQSk4uA#LZR^Qs48lqP*-YDC^Rl?O!x}3 zkm*PGABKzkok!U4pAYtvF!(o)lj>s^6pGLc`GfXV=8H9a@wTgyp6f$LOIHuG=TA}g zX0A?lj;?mr=1lHSpSxH)Ixq?H@bL(8FW-q^ zBhL@(lP_e|*DkTP|COtM4t9^Au)XeW{e-i#wqmVHRE;v7>vL;e=v{ZVWRmoCHD+5L z-FUT9L))h4CpqltN)zgPr_JB}FpancU9wm9PC7sQmF#_Xe}4ayVDAftcd|@GFNx%_ znS+R^+1g#vg8ut^80l5x|GvgD$i7YT-`|pz0}}uHUWX8F&3`|onEhae?Z3YXL|@^B z3&>-~Ta$U>Ab$%}&umabE-L?o=Kp^wZ07%8tku=*YfM>n>>M2O%F39#ySojRW$8IN zIKIBV*`2TNTnRuGnba#gIPe_#NDB!GnOa$4U%e{67_M;tK0#?&*#kX2Y++&H<&_mV zBcqQirB6scs$l0(i=UFRC||vLRm|ltUPeZS)9Svp4+lHZ$qNY=m#FgZp|!PAUESS{ z<|S0Q<(r(GoXy{CM~TSEgTJz>Z11*SOHZ}6ww?b@%xdcBNGKs80Z%9JD927!O|3be zRfUL}I@orUB*-s2E$yY>`H97Xsy7~^G@ZD(IG*+`+TFdq;2zfCRB4>^KYwbZlw;0( z7_3JN>EXeS36Q|#Gs0jl4e4ECWnw~gCi1G7nKA1pdC2}ld(~g6aP#Ze+<;zArJf1V zAJiTU-{*SLnRp2&s|{FLSm-P*E!7iEmRj1|n}7bM)_ACu_BjtL5A9W7&NGv*q62{+ zKG5yD$jr?~+p0=>(5zVJ^o{g4{!L?wq81Ekk`u0dv4_c1SQsYlj%UZl=32gjo_Fyo zZF&gX?&(u9-;`X! zqQz^5sJwXj=yEAuG&!}Gm9jSz9+K`pVNmFnVv2};5>F~ojyz-*vD%$GjX^l1@o`S% z3UoIu`owMuIanI5?5U|mI4^Cfja4@i{$436wi)K=O5#U<{#>Z#LIwBQwUQ@4NiZ-m zGwdnVqLM3#ERDFBd3i4|Cw zrd`+guKqUk`YU<5X&IdqGvzZHowF7^cR_;qGE$rgQ@z#rjB+!n60}3Ym=W_SF_Eb%_m>dopy`}$+d@RzU?vXaEy41W!fh8h|!Xw zKYjX?SXcjFt?wy?l%3snPEhDoW@4+6Qn%>w7pVsnXo6qn4hC-?o$fv@cbqnEc{}2J zB_NGu{k{qHOX=nfTOqul-uYQ4CsalS1`8h_k)UN4k-|5(>70jOw%3a4n=H4T)b}1^ zEz%I6SNd-_=Ko8=Jv^j?Rp zAEFIxeHmVO3KDaEpy#t2)1(vkjO$^|GR|z23K|+{4khIG>>v_yoF*1LSFkx(!9|U` zZ^ZnjRdAj=a{f_O6)2tPr=z76XrdC;kl_EY_{r&oey9;jQ+u@Gb+PZuE41jCc#1kY z@31-E;>*d&>3y>yzrm%)^SbyNYW^=*z@%d)zw?~p+3`{;i}gc_>uhXh9ewFmX8kyV zq8O12k^_YX9$iyEuEO>x+a}MFA$WW6~@=#h&_%IOKPTrV(F<^9E#s}NT$SA(TWc1COH)*z`4jr7gS|fb> zHMJ=>k|yd|{+V?Syae)u_HL4=g=G zx+|g{9#5Vx`XTDUkrMV3NY-Hr0a*wvS9-BGjai=kgTROuw#)fYBK*(4bV6Fz+C z%h#rj`dF*Fu+hoMZhG=Jzv3$MvlRCHls zA*{w%e)S6U_90DueVVhgGd{ZkAtBK%rV76syhh;}b0nTGUW6AATHhqXp82~;aBg{{ z*n`+ac9nmbQg58#0}KJ zKj@u&y{7f>VF^SVp2sJG!HT&`>({O3X1@ls*^l;dI^S`VsxbzsN+3_t7Z`l4U)Biy zu&}IUJT|+KVf{<5yq+G_=5#&pNL|)&f2NA_ZD|Hl%46Ah?z-yk$6ORQcy{G1EpMEi zpSO<03U&Vc*;u;R+21>b#?37r{;dcyus{2uZr6v~*L&sm z8`U>77+Sl=6%-P}a_iQed|zUgXaGQ3=y&ei5p&&UYU?u8oL_0ronMlM<8;dy!# zR?BP6N`zhP`dh$t^J!#H?MXy!eVg(PD#cHIYVyCL#5mEC;Bes#HKAeY|kMzRMPyeaq zYgogEM%bH~Eld4jcwgm3vQp;urO;}07}r2vfo&jF=y#PHXSwS^pZAy}$%;mNcUzkC zeKGfa_Nkf|tntz~@e}~+a*<9PByX zJ%cn~58Cj9oPuc#BYP~<&UbxY!fbz_u*D_E2Lxoc-zlF)v!ElUq>XHkWmGFSxv{vk zWbyBlR51i1ZbSsh+V@^3?+#A(k!olq|J_s}lz^F6loDO}uKR$Gwj2H!9(7E?M8$B* zPK@JpozkO6WGAsSx|zeZHWo8hDu1dyD8s|UNj4Ji7*qr`<-3R`B_+ds)&gFor{@IQ zPgLB2o6?Ipp;B>i`}`LeNj7u~%vi_pF0LAUA4!Fqq5!Ps+IPOwbX~E>s?F}cKJ~Am zKgRQ5m(ASV{Q8tADGLir!|Q}s1z{9YUt?lKYWsgA6U(pt)ZNfXm2N%WTjDd__YD8n zt)rzpO`J)@Kpc>f!4m$jH;CInzW;M9y(|9vyER(@7Ig9N4(XP9J|=ih2k{~Gd^qm) z>%bII$A(Q0EaekN>weJ}FkOi!!PSy)*gXlb$R zT(E7-G`_M*%f{^KRiIw+Z|ErTOig&mD?gTSQ^V=>P-giV@9(_oMSXqOz&j}w2M$r< zIny@v?##>ws)mMikobM+DnC+n?MFkClo-NiYaKL5S?>Slk{!_lpa{DgZZbl8TGw4x3TZOYFr z!jg$-DMQ`gjc97=5a01$=}nzA?GwK~T@`Q^yOloJLR1?P)8KTIl0C;qf;Aw(`r0zB zhypy4-|-@4{18*Y+}s=!FE3%_rj*ji-?A)kv6h4OOn-;8tRL7X-BJ9e3wn6YAI`d6 zAnC=&_T|=7Z$7<{lvlkCh2v3kYb0fVhA)#FfHzK70C`(G-pjTG1#g_cLe#)ir=y{yzGjQ@7qvu01!rBg&iBxnCP+rB$Jw=gD> zlJ}SE3^xyta?@}g)gr&f^^K>`YTB9}DD=tRqkq+QKS*EUo@Av9X{d-}13a)E70J%z z`S@dJB1(@3*M(Kf-~t(+l>oTrglE-C_?xuYjE3{sk1`w<3a0;&B~Ru}ou5pJy8F-C zaTxukd&nFBORL|dpH)|x6yL+Fl@)<;1!dX*3a`QnsKMZey15`L%H}bdl|Peo?bDOWC%ZtWlS3>?iW2x zHaL)Va=i}GkoKAF*o^Bn6A?CSG**ggPVVo`582&L`Puu}Rhe0gn?*C^M5A$>$j>>qOyJ_v}>3OD+(#N zq0(G<4aB;VVp$DdIk6sIJ~KL+e2RIIR5>H_&wjiV|I^uNQHM>uh{h*Lx%q5m7K7K4 ztlZ>2(g<2gpWY&%lJMq#U1-vpTr2SaBCia+;!%}jnT}=HeGL^kIkZP?#fsw0>KP4c zs^p(Pf0nBbKl1|yQTwhdEk$Z@hg!eNi(L5U`+)(itS|Kx$3DeXA>Dn2DZ=?! z1OZ;G_x@DvQ^q*p%d^rO;V~>dI`+f7w{v#rhV10Pz(Cqw{bhZI=6(*UH8_|`tc|$G z*bgPmyU@tV-CC*O&DZ9_fT#Z6-(z}r`WTYFufpCSepoR~D1#Y4=u!XPQZHv<_E&NW zvKi&|)Mcx|FLzyf-d(19U7S_wt(xfr{Cf0AyWZO(;j0k8Nbu)L4bM2wgHX=OoH ze>a?-9N4~j`!-x?`@&b8CcX2oS=BTq9+fb~2s&1?{ZrJ*g*b`#xh~6=@BPoGt29DT z3atgIvs`ReU6#YP^~&D9MMFu2o^h1dE?sV-dp=*%`F#5BvgheR?Y)jf1nnqU^BT*J zYD(ti8Pz)-?yci#Q}v)w@4ccS7@5lazak6ZIHtb7_i9y$A5~P2(Hfk3arz?U-i~2N ztS~CRC&c-%fhVcLo^YcLQ+|Sb!%yDXjQJCr%vVey0&x4f93 zf>g|R?&x{@J}+LpSQg=r`z`aRH&upmZ9D$Cqj4Py_V^e6h4)!Tq1?l)Y-vhwDRuVC zcd~Cx%KW=}PBzuiB#m#O($-}Pzpl~z+WbxlB#39?NA3a6n(l516zchuC9b{ySG(Gz>9pGV}#MlWs;JjN2N_3okq$HwY7in_>t&D4Pg3Y6Zg^Eeh0gk zx<$s}H4N6hmes;>L91@uwFw2p>Ns>I2n2M{j`q`Y=28kEf(dJDm}HyXiw|4xZqX9 z;T65mUGM|gbmlZmvC($#$*vyhu5gugG^`lMiDYdoyMkOL=d+ksz4Pg#{#+tEQW1B5 zk-PnT574Ao$ul3_vhrm?ew4_^>h$z?1jbUnfqS9<0t@m3on8ED(l87jQ?s$Lv2$|f z?Y3OFDLr^V{N|D3X&u9+B>i{Z_D0)3*myJ&IIGb@&UMzVn;E`;VJ3 zfg^HjBVsQMJJw^*ouc^E7tcJF4kNYqRV5o1Vc|`t+4VbTBE$1C5H;)^otsd7d67Pc z3zIGiwTm0U;&{6I)57X&!CtpOvrI|Gi8s51XpW`>un5dPCWn%=5~kq?=i^VDcy8k3ciEBD9k;w~szZNQ-fr&-K62aLzx<;s_n;($=kddf%Wr4G5x<1CzV+FghmB>1I-M7l_3>Lp z+==wzb8ULVxXD?R#-7mrKCbA$$Nsf7m#9nB-EwB4@4NgY`Y=IP6IDdR88XZUoY3xW zhJ+Np^M@$rm;~Dz|KmVBNd}F-SBk#Ix>f4Xi#OX&OxbMd8b@-~eTQzzB0U9&m}*fP zEA!9NBX1Lk@np%7AfI~`bt$Zh>c7wlQ~ym5>fRwqZ6`f`d)pc^g;3hW>jFCwi~ORI#KuHuo=?ELwb?d9&JOKH~Gd2>QWx~h+H&i`{2&5P-1p;o0}Jl);PyOEI` ztFMKB{rbfopJ8F7^$0^Gh1*3mA1yW3%{kqb-WDZrJXI(btJ=TA!~J>^Ft*9-+A>jQ zb5qmp7r9v;$<)YIrk-YJT0h+B)$R7WXiL2`n`Kue{mg;w+wZr zCxU#!VS3PhVrR!CY*Lnp<@oqqLHnXAi#g~m871@0wSq1NTcG@?sHpC?mS9R>_S{o{ zvZsfhIz6E!=(FHo@50K;8p6q}c=li6FEIGtmZHo1ASt!i^_J((0`WerXvq8bG=rbR zlwdGN^YQzr?)`Qb1J#$S%^AW;oH}B!Ki``0B)b!Zs=50jYDB;*|LPSq06FnH0{FpK zuc{}$O1zYoCq+m9nwu+Ngv)HMf`sAfWVI;J)C`k#jc^}NRy5z!eX}aOlKz1K`O`xR z)vEXg__~u68iZAGdHItnvXT|pbcV?)s0~%;>zc}vRUi|Y*0y}$q>H>v7>+5#$Z#>_w z2*kOb<9dyab^LkVWK#O=o08wAN|w}J^YWOiHC#e&;ltI{7cZe7OmvH8B+x zw(ohHXT!GlDd62tHzpH~VOJNeq~zpt!&=Q7dwM8D3k6R@SNk_;XZDT-E<Nj@?L+EoRqK-Vczalb-ehvZ@S`wY z=ex7tDCP63*CIazD_pnlhcjt1{X-6*Dc~{)Yle~zWy;RFaWkDsh7h>&s9`1?sF5@K z%K=%mU4BX4o>FKP6~5F3=@kHBi~TSC)RXwoU|wx*2`43HV2FXiRNmN_p8yXtUY;6&`xVWs3m7ouP(NJj)!LP5@fRVGm(uLA6Y@B!2Mn}O&TUA>4CCF1?vLp!)9%-MWOYPzE@^V;`SkQ->rluH{2sh%7-VVEjx@Rhc#w<_ON&qD>v#<~Z zk;E)5&EWF4OU~WBLUAn~18y%_cJ5mtWo2b8CuBFJQ)9UeDjKGz86_Et%)MdHi0SAIi5Xy1<1WsU6~Dal zPE3w5a9)~SU1bq!>+k6yKtn_8=;;Zmu9jes@#89af8A^uh-q`vmoA})cgv^uusoVU zcCa5WkGHBHehch8l-#DDKXE)Z|1{=5E-SX55OEUwU=tgx{qW(d`JzpKx zi;>Us4`%l6&MqumF9A{x6De;2)`Quzz98LbFvP3+e`aF~`(1|K?0-&v@4ZmGYUxqV(zzA7}t8G+gH1b)mcQgtV(?ImUyH6s7|7-eqWI04 zFg{58()ge4U;#nK=ea$jZ^Y*2>@0VG#GoZr@?`#Iq^$KFe)Gf4mau8RS9NuDiYM0< z^@}k|y|6Hu%|+T8(Uy2ySeDnxzlOcA?nxk?DE9uyyZNUQ{TfJIhineK#x)J4Pdm4v z?nT%GeR}`?eJUxheUG9y^tYd593CES<4(jP-0QcxsPac$bmNqr?|51VKC5y18pAVQ zK}UDKIC}x=9wzC{`)|UF!d#v|?<>{jblvzTXsByPWNQNYsT!SMfEG33<(Z4xYK+O_ z$JFK)7D%y!MuA5yfHB`58=9S+-El&7+lFjzj+d4FuOqvt6;r8u{ZdPh=K6t2c(un? z)1N+i2H!%*+*5WkkF}hzVRoxA4Pcnj~9Rgoi?LaE}r|7b}w*2BkHLyyWX^W zo-Gs8oH4z`#<)TUt6^qlR$N-z)D(Ozo#QW|%;^M-CQc_$}5>Qqq_+Hrd#;zYw^!<=Pc*%U^7SDJ|UYssY)%&9V zOZA@nf_6?!PtRT9haImYlob$1BOoFYl9)(&Kg6*WSTFOnXQvTtnmXpqy}cTCzF{fw z9#5WJuc@gCYH~=pagT_MY(LeE^2q0^SW;(eN@6cDdDR^|H{!5=n`UrWI|Pm@wcnqeKB%D)HPgr<*1Y99qCKUr}HpV(RiBCR>oX{K?DCPNXIi zq$*g|q^8RK=H0st15q^}cCNtbI;*cXKK@hP@2i{^Q1b7(8q*a*`D}*R<14stnfM)C ze!l-v+zc8)UtizX=LY{@YLT1e|4S_@JiOpFscS)^cV}J&G?a9;L>`s`jR6SnR}Pa^ zX4^+UlYTf=s!jkV&|4G1=}p*VIi6KmUR{kSKY!ox>zBNuB1TWD3?Vu?x|oDS=Cj^U zB#)EQv$8OZs@)Zg@7&T^j||E68otzc)M^rcTKsfH66rPVG4x2c;PX9kjX#tND+fb{ zDmUBBo0*}}_;>~tji9srN6c1>n)jO3h6;Z8@S)gkZA841WdOo>W@95-C6)olo>AII z4C-B4TAF6TAz%bPt8LTk#`S2^QYxnAH{LskZIw-#uZ)?bXScLL2@Z02pBDoJBofl! z9j8O`@;E~X8I5oHJ1zzW2J%_7;i)Ip-}UkQpNo)Xan8gj?f2h+rPsJcx{?B;o?Xsubtd-L`JU>J>TE)D z$Hv7COireL$88V-FeyCW{tm#Oqs2rMr@vj)>L7e0nsUFkt#oW?b0b>VI7wJTgDgNc z1-#*lB&ZKQK9cV1V*#R$Q!`&u{Q@&HEuxE7BgcXN=Jz>r8pC6Vv6f!5k#uQ3xAnKE zZGWQo*j=G3Sx{5!y(nxuW9puljv_5wv?BX)7;UHxJSD`RD6)L!u$F(w~x;vHo8+p z+o9j|XDD5Z-_Oj=jWF?}4uMc1s&o@1`+e7HxJ2z+p^j1b?SQur+nH|)qTh^RQ~@DR zNz0%TRw4s7RP4p>^^3DT4cGNCVnFUJ8wwH81vB^qd#LiV^yjQ+EzpxYcx>6tcu}uQ0V&FqQ6DPgV)* zf3peZ4!8)Dc|q3Q&7C&bQvAQSEBq#ny=N5lNy^?yQv4)k^h;4uY*&~12QlX>GSJ{Y zo@w4wbpw#D7zFkney@Ei!-(kCPigq<9i2*~77xD)=bx<_L(9IXbVZ}JR9DiPBhNRf zXJBS#cH4~A1YQ;dVkf}i=^G%Cz~hpx{P~RP`}%+tDio`X45I=12=mJLbf2g|fcyN7 zx0oi7>`Sq)iR!$l30%&11{=`&`umk3C2geM%zE$d@ArMtV7z|)`V3^S)kr>dRJ5%q zhQa_0iBLAu($mqjwY4Sd{IUF%HeUffyG%=cxkTp7SXxF@#!E-Lq#ha)A#nebG28eeC}HAcj#`0r4W0m%SWr zXta9TkHcZJIs+=f`GoqPD2Z5u|Ji*0UtT2pm6a8}3Oi~bPlGCA8D$ve+oDNN#Tr3q z5W6igZ+#b6$j#ME*pyHFU>zO*9(S?cb7U_DqA{*tyr7}x{iI-^g7v36h2OG^N<|+dH;2;iX1Tub3X71>b|gQdo%Du!Lwu$RvF|z+XvVIk z|FbfU4q@Nr;9x&_^b*E%<+)=))hcIvkWZ4NeZ@!eAG7Xo=D@2rf0FVhA|*wal9Cd; zeLEc_PPe~fd&fSoyv-96)pL>qzF==EUuJh@s3i6#_|EgG9i|~CF)1R3y0W{SLa8vY zf_r1E+?sU!o9)d2T}5mcfJOfq&cedOZz(D_~b6q9zYc$! zCITIPdv{R{B@1Q*0EP8007Vl_)q01*#)2u3zN)H9Q&%_9HD_kW6Sk)jc0VpPHI|rF zxuLNU&Bn%tq)5rIPC6EP8jG0NkG7?FaVR>wdUaIaHa`LPEV3MMu zp%Ea#P%HrcwXv~L%=e_c&mHDqE|WULg?YEUNFYl3`iMe8Wa@r@(azK98yQiSmk(6D z;s{^&Qefa@Mkx@{XAFK`|zfr)TgTA`Dnk4SV0m|<-ycC}i0kgTB0u45^ z_VeT2r}+lo34mK6pMa5mc5yM7$Eb==@9~D)>JSb@7`$@aa=95qJH%-a+z9Z~&Rpv? zP>3;TXlO|C(gi9jD^>5_&Gqb>b;_E{{`GHb+5!p$Y^o59^L+jnK4ZUs_v7n+8>;vB zZ)s_H*}2FfBvjtx=gF2O;2NArw3~`hdCz(oj!sT+gC{M!-!rqbf9*j$Us_u7kn#1$ zxF%Xq$3lc{N0`{yt}-w%Osj9T1%Wz!Z8EB3-zfy-iuH-_gs{sk!9bf?4_prl4YdGt zMCPva5`qZ=5o#>hs?4kC9_?Z#z#fuQ{kyQ9}^!T)}QhQa@)$wlJ{}5K| z{m;BFO>V5#TU0rgfxFz0Cr9ex!-r>w6OLow9T^1#1d@0{iQ*x_v5yM@0h>cP{o!+5 z+IRRMQ@L?;^m`At_ZjP=+O?ZEZV2coz(7=NJ$S#lN7S6Aj%2wh0>t$E=D>UNHjRe1Vpb zLGmSp*(YgV{Sst~zed5;@aERTC%c?a?%lgrZa?vEhcgeKMyLUNL$fn8FX<)RO*h8B z0sMStg%2pE`3or-+{7c z8hQY$u3yJ6s&Wk|EfpAKBi6D-;te=-y`M8PrWhC)*uj$`_Tx;DO@Ettwr_7!K-mQ% z4|sOe^2B%ED_5?>B_yDWii%2l?W?Z)La#_~u!Z)1*sAs1#f6xL2KVO8n}2G(DIts7 zfKTqP_4dH6#xpI^b8>M(0Z~{L17q((;a>V88{VHRIb@?jTSM25sl6LX}q#{h9ipHz%oo*Pkl6Y|4OW8pYPE1Ccm|u$1{3GZ@^Tmy9#d!| zVPRnnFw8x9KBjJD4!Qsq78W$AyRLpV@-2vc=)e#}V@XNLpc@~amFOi&z7T*>ShHv| z2>vzWB=*C7<9**7L-oOdfhK^oX12DGuw^VnJs1*XZejNfC^@W+oIOdK4z2y~K@3+OmK7T5-(M>EU;SsT+;Zr6}7 zs&ZRH(E7&2cjzjaJ=}MU7$GHpR9DZ#euYIwT0mmk1Uu-nW--8g0W2&ZObyJgi){@J zDBzSxjO^9N!E?(1r4SkT{_|I>U`MthK1%`+I*_6C)2B}eKbW4Lt~%cP>lX>`Nk&c8 z>Hf;Ypy1$H*j+#$^gtJxfswxSFx(TuD+AgLxkc_%dvSR!|2}Y$t|q^JO_Fa^O#zfK zC2lo1LEE55thtR{MFA%dT>hP5v8kfR`uc&Li}FlFK7w?gMe07izDE}D4_;00y92$U zk#$L;xpYWWgXAJ&h)&9@oP{YG zFn6{4MpLm_b6T2g=JIMTf+#vVu;FT2ZVES30DoM&)1W+hPg|N*KHnh-Y zu(na>tBdF4=4OBuP>azspjUw}`(8@Oi@m?I$S=tFOHbANn{2o{Ox1(~e2Zuwrti@N z(E)51P2l}D`0k(tg$1b};iNY+AW(-3LskD1`@OtaZr`Q`HsvY2c`kBQ{I z1AMa4nn~7EHMb#6xwyEf^6ytUi_w9-P=ia5Ijp%M0KLSl8S}}LC&04Tj+;pN-}X!F zw#;-{>d~Hp+F1>4wB^@ZPS6>8*#^aHhf(-VcegZwN(z zSqs7zWQ063CW?-p9*}`j!l`aRU7hrMQAgvOA;La)fOH2=ySSpFErOi0*mks#F{V=e z{(Xun^M9GMc)x%DhTU%{w;qgp_YM<;0=uaiYG@^7KW&)?)E+)01&Ar;eaQ9t^=oL0 zaU7Wv*x2#>muGu~Fh2r$p<8kfrfx9AsK8rO<>znCI*Ez-or!r$8h@;sy$j!ieN=`{ zi14sWy{Skidh6GBEItGr&3Sb%BrYidqjc2Quk7r%ZkdgjTT_XAa1BM5y3PbL0>4S}YBc!AgZ z;kHI$+4CX1tE+2E`d?>*t)(TVy}f;U>Yk@0GE2jBg^-)dQB(my#?5}8_RB0bAp8l= zvB5#ooSYowT?QA4wAg^Hk4FRl(%27YRzO$5b#)cR#i+zicm{5&{K8F};au|aZ{ODb z6o>F0Ecc^BZh+k-2y_I*>%6}QHE4FRGm$*DprC;Bf*Z_jfa=sgNm3i~P(s_lCL|06 z>UL;o==vc|*gnVt_#p)J5%5gR5)vsZ&#{l05kIx_)^vUQ+3_ARDJg5YFTfZCJPHX5 zQ|IPyBDnP1H(?SI5}Vqu=n3lT>M$di+1NxNsRm>Vxy-3}S7Pk+y}Ngt;GeT=Ymv~r z!Q{)02$C?tU)Pt2HwHX&C8Zl_lgT{6ag2!2P5nlKoNqTOlnWsR{|MTY$%%)s0 zMhNJAw1f5p7Up)?=OmubI&gBS7zP)FZwDVKMq=mH*mhFf8kk@-A3X|+jU_~OV{XnI zBnr$t-J)zqoDNaIwES+Xj|^)(A3W(aAZ6ikTjIf!4~v7z!D6z?4ekTXdSM{}gs1Du zKyc%)Cu%?rr03^H1qB6(%S^rl$`N`em;-Qm5+k7?;P~pFK%jic#+F z?r{m3Yb;sM0WTs@P{eU63Z$EXBnOsn%j(Li^beutfDqX{I;zXQPD4!{(7C8JM$Qjx zbfHo8$cl4~2`xk>Byi-LH)QGQ@DtQXn{H}u?gz+>TfGnp%n;I0I(y4$gQ&j6BGjTJM?%gZOhHgEm6%}$pWzDTcz zj3b{IvlF4<*2j4E?AiMxqb?d@n;`d%@mWCD>WNyMK!3FTsdOoU-EgEB0NJ4trnq9u zZW;s^K4Q+v1LQFS^fy$S+124Z^+bgZQKU{TBByg;(3*$yCFB#3F2n$a;nW!>XP!xZ zL4h-L3~_2qfQ!Y?7w^KOu!@RO0)rY zha_-%{=5ldgCA}IM06AQl$LGWLJ8>83JYVTeNUL+qz@!tC^Xa82?^BspN51%#@gK6 z?0^#raIEA1zOy_`T6*PH{i2=jux*SN7Z*U3nSCui>4c7niHYfEUJ(NEk5J?>F)>JO zZHr}$1ugyTY_At8YC8a___uGhcg}WOZ=lj(h{h%$PzIU;c{0FwpTZMmSJ@FMK}-w* zWS~&N`l?=q@}Qaw)EQue8R$>s6A+Hs_diC!au3q)DN7e4?Nfh2@e8D zc0`*v`L`8|R6kI!UzV0WZ{4dS*f8O_iHCas{=HtkpA;a@P?)iq0eZlh3znjQAzwfk zAuy^#d!59%lZ6SuIjaK@=?v&pP)Q=}k--G|4HC%3dR&-!taJoepzI#~oAyuUS%hu* z)3CoX2w>GKDLL5;TAGuGhYDbxx)+;?0i!J~3)O?aOMcsReUhRDasoMO14PRMO--he zQQ0TxDcy)bi|8*fQ$SW=pbE;B!ybXrUCecv2szT=s?_p7OzGwq%=;LaZtt&od92LL zayD(6U$U|g6ooV~$hugNP1E7+-%87Ha;mWx`}?~6Q-iIoKR=`e&khoau%rkJ13yT< zV#`$8J}^L1>vbRxM<%K~w(bw!AZbI+CQ<1a8b-nJYYHGv-t0SwM4-l!xql8r)Rg@E zEOWXoK;Xsh8~W<0()5oXKi=Nik&~5e@DDLUO{*6iE*?$(a$Ot2habX3A-f2eRG9a} z5V8Q!O2AfuG5DDXIcs>s_h=gpFr+dlfPIGm#!QEEwSoD0_EkWO8fprp1hb%ELi`1& z9z+xrAwX#$$QVG4BAmVP0)p%_Gp|X=0Z{#oU0wNi&mk|!;I_&W752z^M1Xy7+Md0> z%*xBVrlO*P^hr<`A<9+}Z$2M8oaO{GaE@m_FkVnBEKc@U%9DcLtd11Wf))9C9kvok zw!rT3A+j?3GPo?{;OU{E=`a4fYi5?-S8fADO~WZopVG8!dzTj)IO_*Q5*?OAX-{Xl@?S$JLxa6PzlGkMW{uv zQhoUNkr-Yacq2GWdIj15z~XRR-iOL~PqPd^my!RiUG+D+r2-@6&r6!*DG; zIx!+-9MR{0O?2mf>>?Q%5=zr{ppS@(IQ+o|po5vqA`ByV!~9N*`<{-C4F@vILw}b8 zFArQ3A}VJXKMHCeoT4F*t#)0xLQeixfA}{D!5COr3V`R3t^q*PW|$$~=wY@x+RLp^ z=s58hNcCudempZfyVI>z01|^B2);J-l3`DPlSv1Z!@@Wc_wggXzrR0Bf-}HR0-+$U zsYxX6x~#cY0pJ5jO@o~uH=3#IAZD--%n5QdG(PBOR{1xMl_9$k&gX-aHwADTrd4!W zg#D5=L1Dk44I_3UfNqX{hl3h2{pKWYWTDM+uC79WoXN;+JUF=7*&E5s z%9Om=vjoJ@7hS2CfgIQ~X8{DxaxFtC6z5@1pwQWd9aIV=yvjTMKUz*yAL(nXs0msM1X%<|iVvzKFNiAq;+8RO5 zCnVGYr2`=V5IwtKq-Y6mvp+L*?BogxoS87oB)xxc-WYgQ%x(3J-YKhFXV#sBw5+Tk z*i^6!=BOZTUmmIlP%Wed9S*WJ%{s+>`V>RSYkam{2_BJ`z{di$ zc$FbG6uOBylr=s$Tr5 z9l;!b5x&0)j1kDT#Sm#o0k9KES$Q26g}C~Lv=Di6WBjU&j7)m5t(_enPokQcZU}Ta z*NyRm>UmGZDgy*7;?#o$X<7K0P~x(5e}8RM!yJqdB&4LlZSj#d;-#xplAe69DWO9{ zkA75EG7Af9&j}N)G?|wm?H5EsV3q`o2y(CxsCW%wP!A4Tp+*H)Bu@XZFg0z^e5$Ag zV=3Sm&Ud4Wd&dY1j8LQCd@~&@HXHk9ONq$W9oqT4%|CK;b5;ND7laegivuZ)4B)VP zn1#l*zf|cJy@1f0ho0UJg%f~pqw8(OHiZ2M5fBvQcloPsSovJZsu~P>FzL2D|1K3T zy}0+=xY`{bVg7;5ggH43$|j=qA>6lx$!y}AH`oQ3&#bL~E{pB9MbokZ?t<)W-3EZxGceKsG}W=QDEp0gO4rxdsq&*#^`Y{> zY!Ct54FV`Xin}#|sE9aVv(=gb^1~-9=RtP|G*$x#eYSNGC+0g`W+Hjg8g&V#zwt z+17@M42J*!VTG)NrCBHt!w$#Xjwf{0J9rmCN(vES?RKb{Dyj>B2{139a03#3J8$=56eCwf@Sra zfg%O%^s@eNQ!p(&{~G!>tvr^|16d3*Lt1S^B{Vnk;_t)S>sL$AIL#?LBf|8EAILs< zZ0&w^y_RW{!~AiHS)($qmZBHVvEPsF@QCQ^33yA#?bikW@!b}@om}a{3+AxAM4-JI z!Nhotk|%42bqx*b4y=C@_yD2iBIGg*NPHmJ!cnqU&9etOY&hK2@+x0!85WLh$aP7RbkWZq}2v9N{*jcOf~Kq)u>{$1G?cN>-s zIR!gP?(qie2TzxHTL%t8yg!VB^O3cK1d%So1w35-rQVVKybf;W)PB5u+ki; z3;(T$b|gT)0cJsd4zQu=@$Mo!JG-ek7mGX?L6K<&G6EqPvYVN?6t40zr#3F_m4Fi% z&{js5AhJ3-6gq{5I56!mudVqz zOYtOL1A4TC-BV^$jjQn7!*&p`-qF#KEF6CW7Oe?dO%g$PSX^c%E66THbaY*=?EkwZ zy5m$$BZL_-HT7eQ<1k`YgqkSv{`C?2zMr2o%n3L!d?LyKVp5frMOC?PoOB!Mf};Z3 zs~YHf*KUvVo)#}V41Fbrv4$$@Z@ZPPEwVZxAt4B&09Nb%x%e6mfLO%P4B14P!XvM^ zi|n^QD0$$6eKe23$;ygMz0E*L8S*4HE{7Gv@vm7Bq>I0OYXw#lSa3anXBL1_r_Ym9 z-oKaE*MHyNPXl3MBWY^w;6QNe)=$Y{PC!1Or-EN&tj<>g5Mda!uIy%@3+%@DmY0_s z+S^s^7%v~c<99gW9sv9Z|6xOSXlq4&K9{)rI(#xh;7GNHquqEp4gk^AKjF;l zK-&iWe^d@na5)Xm65*&^<<0s2e@IwqpC4_zpnaPIIzY-Dy8sIpu#?CHSz^`Cf_%2k z#00H_gTobHqJBteNru>XtO9_V(71CiH^)j?kwcygp~di7J7z#hz|-;n?fDo9Oavc2 z32`Zqb7-iGlQrHDR_50+@WQ;{%7e^4PRMw&yYLe+iR49K>|U6)AU;N@E(rW_b87|b zk124C+ZDif^+Wejt`e5?JEH;_Ht)h)z;gMFoY8LqnULoqaI>t_(&@_{@|jKt23a)WqdYP4aYbNFJb{rlw{C zqyJd|=#mQh`aix%1IU2!4v-MJyyKd}YoTg=BZR?dZM_OkX{7FeMb{P(zU+es@hg2{ zM92q*K<{}4%pE9UD`@7s3ILsRkkcOk?2GMwm(sKr0wRM3|8?wX{O;~^wwh4vrylPi{bEAKu3|Puip0R$_h@FM(K|^Co%8=3+WW-2ak>E!MI=D78MpG z1q}^gU~o_n1~QATIE4je1SSq$0}2a~G3)F7qbE|~?L%R_(66xjY3iz|2@iyfoc800 zd8iq-6&PSRukZ?(>y9}vrQ*LAvSt}@z=t9wY{_P^Har=doSY1{3@o_n@ZbQ+q4h)s zH@F1@fogv8^eI0*LA>mvZITh>BPbx?ond&R>BJ&rh&osso&S>RXAW_tim0^EZ-ISG zD=3JDM?wg0VB*MMa6x^fprC*mg5viR9LCu5iEp;L@#UNZuCA^KP=QxQRvmh(srYpk z3^?o`0=rn@INjsQu6P9sh#oh%=b?LpZkA`<1|DF@G#ExB%l+AiV_;?AQ&`NwQUhKO zz-;J%h`W!pyQj7Lkro&@`qLG#Fj-jxk#+@)1_h5)e)h*pX}n77}^MI%;g{rD<~jZC=9OVZBf(+aD^$RHGAypl*S1_K91LZHQ-6i?-%_T-5l>53#VS1fY-;t z!9n^X{1)^LY-|wZfQ^oW4g~lJ*>TqK^_1?ga9%HIi*9B{K`j=!xP6AHqi_i<5Zs#s38PUtAv!l1c46;gkT!W zt|T-Q6BER803`x(e9;J5HMn}341HFkgu@3kWvMv&u!7MzkHkGT)qMvq2g|J=BEk+b z?f~Zip9#cS8j2n6gcuLNwo3}5_p%_{bP%p_6D~}-rwb#Wfry5Bg$ez!q-ywHoe`Yb zM{HLx_+7;F5CjH4ZQ9X9=_dX7*uA# zeFMRt`SmL{62yqD6Xx4&g;NK}u^%udMPDog?wj#i>=ZCuC{d;jHq z>zvm)=k;9I^?2N`>y=f3?#a!~<%1qWuuK*pk3c*QNJ?H^iW$)YS=e`Elwt16ZeXk&> z{I~R(R95OP9xp}-ah~hFwmES6z+^^*>nD3J^X(o$k0qEpEn{QUh(-lDA!3FBxC_?w zEe4|uLB|;sL~^pf$F_j?fp9SI*m3pimV43T%3#tcWF1-^Q0ss(g6D{ z0kl|H*A({!JxDPGppJpwEC)Y0?&sv>BqBV6KlzF<*0&lFP9Y+Jp|b*17a4umG)131-CN@6Gylt3hnfI5g^YG{h{3!%Y66zFUe}P1na3?c;b;&Piwy3llKm#R&dV`2% zM9;z890$JF@fBWd5CI~PGir6@V`N-h2SgMFC9Naxc{t(bdhM+yHCu!qmwG+a()xH+ z8WpMxav|(_L1kt92m_6h@=ZMsxkm%BE+CoG{}cuMf5u zvNk}@r99u2no5E|7a%?)&_!TyCQ;0T1U>Cp(;P$5w|WL6L{^D|H%U4oP&q`3GGN1G0%tfdl5GN=a;AoE=l5vlfh z1uGZ{i(z@(qMhs1K5^HD)x5Oy1SnHE$X$kp)+J65$OZ|PLHxP{ zTW=ZE`K_9tV#9`l=tm&lfE<3jv7 z+mmv0b0b@$em7UO{4o0rtq1#)R+wn-ZDu0d#c1~SIUfCofNu;{^Vih!Gh;0UmZ zRD5HjiqOb-xth`5E)V+0;?_K$yyAFz2*M_fjWzJOsLC&5iWMMfuI4Y6Xn$VJ?Lt2 z2<`Hq!;w8ut|oANBPoLT&9X$O1>9cjG6$k~blB)B71e{~ZfHRTj@<)u1{~=1S5fxy zD+azcH|zmPEqq*gZ{+-50|o?YIHBr&k0@c>0%7uXn;-HiB8>7vf=jOZns6%L z{HZrsLwC%+p@Vn#H}q9s-NJ*}4=DD$NQv&FM^UizBJNN`TkG@mmk$>AI>949+-yR= zIpSFWPwKWJuk>NAND+>c`MH%yGzG@Mg$sYP^CO{PByKnOn=twq1!iLIcq2#IW-%UX^p!B=|(*^;p5iSS(!HQdUWS^vP z25bHBfs6>z)rwE=Bl*$j%jj8baL3eN3V;TvWn?WiEszJV=Q0Gli0la{0OF*(L7S%q zDPAoXH#d3U6G|cLLH5)B{vvV3zJHfwxy%#O#3;S7Jz2i#E_Xv-1OcgjLj#C1X0S9# z&J@>mA3nV3=;(+nAM>vG4mfudhAh*%7EQs3)cfr6bVKX~=1fZQOd>%~&rLy7^R1pi zoW&#z6QX>A0@l{uy;8850KVNz@Sma4sNloY{;8L}oczU^U(eW>< zGjneXJs2ICTV9I+R4rz%iP>EN&+#@w{?hL6fB3Luesc1zbVYkHw1F_ch>O%rp(l%&ojnIy{8;>=T3R^v z<-9vNpVf;td3m`M)}^&3J`LhL`q+as)&roIdyI_v3Q)P{XQxQ|0$CE$r4X!{=MH;* zNW{B7(?EZOhgU;;IYcHg&%7D8&aoTAHL>wKsBek2!tP@X?7AQn%>ni#m{dgoHY-qR z$dLkDI1(nq6~o4iKx<)FD_2nMBPFTI2*SR+KJcJUp=k4*PqBPh8>#dKF-4{D8@lx& z%ts{ZfutPVoys?v(J?V#x@#k`TzE!5zeN@CuDD0B`|S3SZ@vEx0fmIG^XWQaEJ}!I zs>F|B1Aqe9s;T<0srHjveZ->1QdZJ1CHsdz3U!8V0Qu0FsM{? zJ$=mn><{YuCu?r?%^5r{TU*p^7hG6kR9>+!SpJtfA+a$;)>7W507}sCIbAbq@{e+-R5$@DZSAyajCr6c`r6!9(@;>KUzYWSR~JC}p@T zwZz%#Ne^G*c7Kfci)I+~p0cd$1>g@cnVDoD1iGyZ$e2t?L7fu9UOV7Kw8HRqpU>$m z;ftIFEf-p^41K4vRL=~D;(=zfmx7=(cECDEKrlKoDNFch+iC~Sj~_z6RGC;++x(TA z`qtmq+$kpT{A^Y1?C77PkH3tnR?8PNaG?>+(xP}fd~d7vfPb>inw!Pr7$|WnAi7WH zH=xI*`On@YsKIlN`w8}33+N8anCgz4oSeY|U0xc`jUYVz3(ZA~=Ua#BBtPEAb^x92 zd^o^-x2veZbCVIQ-z~s6Q(wPE;*){V(Wf^VmE9ph*afY6qi(PLq+en!9qn9gUTU*S zL&7LY6LE>LFvXl+;5r@4aiXZPr9Fr9~d@4rUU~KvOmquKl>~q(B;WZq*O2j z|I)}53Sn|GG8kgx;>sZZ#6ogbAK;L$3y~HaFhtaPOy5ns?wf)?u8h`9&Wu=D8wZ_o zV_Jhipa!B62UUC-+;5P^0rJ$@9u{EM>?ILo2OfY#xxi5A(`h=ehO*UOZ=MUCtbhLx z>jXD;^hLV#d67rP>Y|zhR zmz4b2!N>xy3hE0GvKm5^m64CbWLJ~G&i2VfU+mOds#XaU%Z_MPqet8~(1ymnU%yZS z-4)8tTOdfxDk&j@Q=WuQk|tq+(OhAG+3NI1{vZ49o}3tWX#;T=@`y{Q=9)6|u*t61 zzS6a=Ut}S6L6V{4xYx1QRi^Q3?Tr-hg!|HhQw}y4oT1;_=xCGoG;mRn9h7zUDZn}n zh0;- z1Nr~dN&w2hjFG2n zVK1Ln?!y7k+m_qhLzG6Qzf+l4W~<3AF^XOruurVb3Ql|9OBncWDCh8xSki_ zGJW~d)0r-FyXh#fxUf*($%zkfvCO@}2`K}zBujFa`3C3^|FM~W2CkpjO&D?7dx>jw z{jC$(7Ufa@;Aas7Vgc;D5Rl11-*BWH3{Dqju259I2FOlx;NG}LXe?Udm}gbYi`mi{ z{iuuUa(1>F%8k;Tyk@+=H7|!?(@=#4=Qf-F+0nA2(Ku}CfG+7Vd0gb)Ym)LEJ3vaD z+}?!i9-Mb9*3|e&(*WO#$Q+vIxHrh*V6&J*D*f z!U5A8lE4*dQzCYr!~0enON~A4aIg6N*J0llsC)YRJ2s)uboM!U(B4#0oVvc>kAyS&WsQvBhSS(@Gk?@o zRFJY>aN_jLkATny`R?Yn?j;5k1Loz6=p+3Lq0cm3&Hpb8;IV*Ed zxbyV~pQK^_Ekag^3>*~vgc?I*2-toQ2!ZYF6U9}5^$kq=o1mlDonU!15hFhY*qxM2T}>t5EDDQla9twC5 z9TZR^z*cAW30|hBCkA8`G96uBU_;tKKpli)kE||sJJ^MSA&NfJ+Xyp4 zcM>%;?3CZ?qc_8I9gZg;BQE&654%(-C{-*xJePpwNENoHdt6C5bM=nj?pzBFQXU|n z>lnNj$uH|zQeQ~oim#t-d-Z-OgVfF4eWeXZY><}Mpy9&T#epo~a9yVnX+i)wjS2d< zubNK0o5*Ic96lLQ!)@o*<-b`aj#xh}CiQA>KVY@LSmfT86nUzorVKuJrN*wje8?MF z(dZ9UugI_7z`W` zgAsuHLYGso)xia$U>d9$!h(n%00+CVj*1>Fi z)kXlt(gpx1#d1(n)Q;>Q0$TO|++AjgdqM29Y2{UGS3jbJ8+p_DMi-40*i_unBd*322LF*yB)yGBPJt zmJ5D&HK>mCM*TWfW$o4tf7H6G}+LXn9e3G`x~awoSm6@UI-di@a|N-fHuJ)xb4X%o;WCH%`ThOz5gwpuL^64P!Pi-DP)@xX_uh>CePmc(e+D*UM#D6qC~SlW!`5< zb$&I%O?1c%m@9mxuJXa!=KsJW2q95XsSq$KO9icSnAzCE_6-BBbnin8UuU~<;r7|4 zm}N3Et>*{(1sZnMn`1aB8jRSW32%oW{eY2b3pj_MZT|(ZF)D1w)vr0faJ))OO9(c% zc}z({%dyZmrgoN{Zf41PZw5#07XxoGM3bp*tu^0cY#1I!0fvyPuVEnNx}Jz)?gRhq zaHDz_io8QsR;*}VkKR_4qOS5I-gh)g_+>w7`Zuck@uJmi5eHAEM9VP>C^J$xk+7DP z1y<{e+7I80E-i;$P585D|B!uY>B`&bTJCUlXL-xD#>!&Lp&|~rAq$Xp@QGYCWHQsf zY&f6f1fKBW@}xAM;M$E+`(vIrA(vYuw%4A|npTf6fTW~nFHMoK**;PmU#8&sGC2t< zCQ~Xqc(8y3!YhiMl+${|C)c;M{A+GY*pam@g4Pgwrkfv!clv&V19T?*aF$um*fRKE_U`ojcoZRKuzcnMzcZj_> zg zM%~AB#~FQ2M=q>jMH?)Ob|++U)l>4oyYG?G5!jvWt+;Y9cl^x$hL2xdm@erPM-%hD z%=39t^3M0YeE-{u3JQd*mz;MYWdiV`rMvKTbJcY;7ygyX&DYyXGLL9s_Lze_fDrTS zs;Pl*-(CZ6IxyNW{I=gQ747fe$N-=Q*Cvh+|2y-XUM?8e1l zGMnD1rTzu?=846VW~D&SpICE)Mf8zUoiy_ydn3I^6D@C>Y53PD&z3yw z*7h0Bo}CU_j@xEu5vpY*7xH)kfdJJh#0|#c0t`eXb#8M0GT6Wo^ZTE8r4<;eK3G;# zoc~DxlVNK8rl6+vNyou&&$8wuuJzlDY@?vGwj?FrmB4x!&#x% zeE>uytsrhXdYLSALCxk?XP?oo;hKkO< zSh?t#y2lu3{Y5vxIpbH+)L3LxB;%EO%Gj<&W+ApmZx=>ZZ@M30ejbKNL7-t`~L9Ag)J#|7AfF6-yY9eqDgvq7q?UXX;-Wb5GJ?X z_1kXHS;p(7*k)Nl^?d7Z!Oo1M`7MesE#$Ccxc2a;l9^d{^8y8j&S&10vUvRO>LIps z*nN+y@tRI-;(G5f-e|nFl$svQLMNp4ZV@8`)1efIO!$vr*CaV9MX|&3m?kMDxiP>{ zz45;CFQMGITlx>*W#`5J5Xtn=|L{03`^?%i z;>POZel4h70OBU71FcR;c{z){+0XFmja9n2dn&^{_xo7c*@{}D>3o;t90l}sJ+$u1p|Mb zRDZ54p3VJ;)S%nCvO@N!rr`8dcvFN1Iq>#IQzE?XkJ^1*t$Vu=AI_F&qN96&>m8=6 z`;DB;yg28>wWiJx+wFrH4f-G2v!!G11;w05^&`aH7h;rA5Y3?l&>JJJzQwOjOx z-L~liE7+10GfymtBo%w_sg#ZlQhzKlHdB~uS#1n7Zc9oIPYY+C#v@0A&C>m~mvZ9D zOnR)_gNF~z##et%RQeoolt}Cgx4l&!=6m|_pXK=SJr%j64zZopJzGI*_LDjn9mCUT zqMfEgDIqm($$B5a)Nb(-{t1s`8=;NE7*NlT@Jc+7f$tJYs>A?#jWI&q=g+! zh?V&eMZU49_k#>QI5P)63;adX5@-Xkfb}-&u0S~YsEM5}O-WtdLY!oW$#jVzp{p(L z^Cd6pXZSq&Ap?J|4sSkgD!VI?o9pY(DZ}EUlc|uH7uJ+dzJphS3*16bdSFD3jqMNx^=X7j$-(2LpHF0R6jThvd$&C1@!$h^zpfIw%MOSigvhn4zL3i){_{c;^9tOPYvhH0@-8*wDPrJ_?fZc3q z{4bRx#ubuT+;ZTys!Q}j7lJR)q^4y#-zTl__X#QBV0pf?BW7p#(ARy5^!xeO?{=AA z9~o%D{0FV}8aMZ$>BsbO$Cfkh$C2WU@vZfept^NYo@>&0ob90IhHedwI+v-9p0{6V zJ(FUrf%xk@;GOWVR_k^HQBtfG{wMN9cqJ9FB)v^LPxvSpOqaUX&N~|#8N9Kp(Y(Gp z$KymsJi6gYT{*e=;zsW}tVCss1)sUf56ohwFP?;hrse$S4(HD7FIgF7T4{PZ^M&B2 zD|XYH1#2oG7kpa|=~|R|e8+q_5gzbl5DXlh(>pxrPswmx+FkwJ>VaAFEaK?!%TxzJ zcsam7Rv zZGV1esC9N`l6>0Ne&t2+YQRF&=o!!ZS7YswqQ3|dbiej4YH8~*lo-Fxz4OxR3Vf@w zfANJMup-)yR%s^XML*0XnLzZ%$<1RS9&4i`_o@5KxRh8}ZvJuRJ(YILfRvQvQWs;D z8_s>0a>`x`XZVR`ZZkZV&hfxh$7MK(*G95&XI_YF)A z3Mwj+7U;{Y&`1O8j)xCj8|K8=!{lrfF*m7~l~fF;SBd!-=G1J69+$fr zt6aF@#ipkNZs?14C)UlUd91+tU;~VA9PkNu$a?UCWBU{1Ws5v;24(O{QmqHG!zNG( zVlUBV%J0MCCzg~v?utXGVCxFhF+QzXc?tG4E+N9&+Vvmf{|!_gJb4i0;#vkn1-vLOcG%Og%hU1uW!G>E{qZqWeDsgjPqI!?=X2RG1?~-AgGg*A+ z48vJiUID7*?*IF~G|+G-FS$zZNw?bT#~<`XqTxO~iTa+vf*iK-Vnaav zhNCIs^M728*z6u!V5#r_n*F~cw8q1JiskqB%Xw{BSgc*r3RCcmy-RXd&L}$1D6*9`ws&ZLWTZih83>w%!I>5YsY8_Rh z-)H3R>s#l)6q%HCMf)D`V_wD?MJ*XAbAHL#&lgGKEJm)XszQ^Eg$}fZ>(XHVnpNJ%C@^sasBu1K zynDy?MmqB8u6JOBNQoSDWd@+h?B+$S533dj&s!Wwc^Rh_Vay9Q1Mk5?dGz1!*ld4W zk@|{u9JG06ZvLP>U82APUE&*DT+Zjmv#}jc=aV{m`ub%aha$=lEF(2W_=D$sLsAfP zH`r>GR%N3obVtP~+YI7=lyHb4c3$Q6_gjj_L+i7Fh z`R-&B_zcp3=t2dIz)&M+rNcB`US5W?n6ET?Lps2TjF(Be9aL3X>r7umC}kWdyU$@@ z#;Ir|SsyX3u>fPK2OQb<7hVSE>vl}YTgYR^HJHqSh(=@#DCua6`}-c$gojJ^(6hkc zl5`+OSQx<&5E%o^-5|j9vK?prYn+^_yQg?~xjK>}LrPyu(gF_6;+RPYaN*&>>eDCe zG5!}b|KiJKP1lP-`{-*sRXx?NZcfcjS}D(8u^ewpe`6p}_)E>b+CiqN{A*<;P(nAg z2lE$SHY+D528f#IdV6;05fxFy~2cfmCKlVl&07+fe>bcf>2yFZtisH8pYfVBhecA+uYn)P@Xa5@d}9+XqnuOBNQ9x&0smZEsl!q8L!} zLvKXOs;d0+t~jMc0K>c(PW{^QN~Sa^+8dci@6^y|BJc0&wd3 zRK5C&JzxR2MTXesi~CTEN#XNsp%WUm<#ffi1of{e+)`Ni?$@HEaW8Nx8es4kf^Y!6 z?#9!Fo$AXcqAm9?GNZ*XF7m-h&nAl{CDcSYBb!1i)6?a?H`kuzv6_Yz6bwvJihw#@ z@bL5E*QA&lPwsmw8}AzZqdxr$E}iJWgCcNppJz*JFiXMezEbGosknrZ^YYj<&Z=_X zlgE8?@{IVk$g}a+{w7TNO%4pvJG0Dvv-%$pBS?Ni*b%8X=aX%^Zu+$w@KSP+Y;f9>&s#yU z2v+?!iUKK3t;^c+H{XN=udDV_5at4I4b;;5k9vx{F~6+rJf1CKa4USTO62f~(9fHi z?perZBtV-Qu#a19+kD`YXgq%3cj>bbsy;gYm-7P~&`moI%+{RNR_FO!e6OP4 z?$^QsNceJxwW2wE=NZk_0sMAM4hEue_5j5~<=BN;DT+owXylA%?^ubsz}95O^QNN; znrGEGo5S^AAU=#r@=ck;OH7iDm(g1r@7d~Gi$}lk+w5AAFo-{xo8pj$=qZ#XgkE4WS|SUtQac!g^KYAn>++iAWL8D4p2<|B z`;nwa4Noh;6#UReAjm!?@?jk6$ov+ha=29+`YGQ`mx-<-W!mVwvE&mTQC2;@yxt=8emeG*}LD)eh68U!8AJzLBDy@Ng9nnB%v~EP+-||&-P?0M9=Trc>qYi@`5fCw z0R+P%pc#bwL1~kVfJVHT;T0GsX!9WIzLR^$mGzGY@^9?}rfo=Pc$fv2R#uVsIW!hJ zLiB=ucIW7elv)>~k+rS=5vu@<`-sJ^_$>E=%z1vl^gHYnH-JJ&pj~9v>1hl5yZqZB zh9+C8{-%YsbreVqxOS6)kS7j|ikxKeD!_Rr=Wj$X+lU9pOulZSs&4_@_#D8uCMq0A zwT&9?fP4?6+G&>k?HXrNxX&$xDwe3I$iQ_nhlt1&r!!`sC-d$Yke~c7fPc02aXgcwc#_YHT8-4{vz3 zKesd3W;I)hPbYj<#v&vlfU}m7ca2MoLYD=NpxfArD!4(#TjewnZ9&lMA3?;6zWi!& z9ec3!)Y5NUSlhUvd1t$gywQ&sTy8$7KW0|oI)Z~M5Y+R9+3vr3e1#$l4gtpJr4T>j z7CVPcEn)aJMku-x7(&3+c%IFsN+W1R4iJ6F6ik)*O~1xEGE-}33XdWqqYQLcJYyIj zG5_~v9-A|9(Dl`%rF6z?XR{yD>xvqJI?1!psUm2P?S80LvY)IdpUJB}%goLhK<*LZ2&)bG#uz-Th zYfIsT9Cl`A265@(QyuI+`J*DByqk3x!l{oG(#DT!%oY@KCB_BYX%!WMPzf4`LEG~|TLe0y zf_P|3;|x8tk>+&mJ&ByhR-N2(a_Cr;H4Z&JJ+LSPA&Ps@j(Op^EhIb_gq7uG9>}=I zBTtG^VCt{gGmRe^C&XE;BXeCs-hRfZ2t7$`(-pfD3IHd8K1R@z%t`bniE>nwkdY&# zlk?Ci53USN?6hXB2xS4IL>?gx(jK6PGT_SrIU`7#{TygzO37A94ALh)g zhms?IjzX2^e}5H1K?D4YEP<&I)Uy2dR{<0>A+MIziz9+2i||*g&HsCa;FSL^P)c{D j{{O!e!T;BLb%x)?_OCyF?29B8{HLs-E?+Eb68!%FW5_A` diff --git a/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py index 5fa0c3e82..d75ec7c14 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py +++ b/doc/source/tutorials/bipartite_matching_maxflow/assets/bipartite_matching_maxflow.py @@ -47,4 +47,5 @@ vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] ) +ax.set_aspect(1) plt.show() diff --git a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst index 97b36c08d..3d00abd36 100644 --- a/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst +++ b/doc/source/tutorials/bipartite_matching_maxflow/bipartite_matching_maxflow.rst @@ -66,6 +66,7 @@ And finally, display the original flow graph nicely with the matchings added vertex_color=["lightblue" if i < 9 else "orange" for i in range(11)], edge_width=[1.0 + flow.flow[i] for i in range(g.ecount())] ) + ax.set_aspect(1) plt.show() The received output is: diff --git a/doc/source/tutorials/bipartite_matching_maxflow/figures/bipartite_matching_maxflow.png b/doc/source/tutorials/bipartite_matching_maxflow/figures/bipartite_matching_maxflow.png index 86a6fb1bbbcdfc709431df4f216d7f8cd1433c27..29a7e5ca98cc2e90dd0dcaaa57eb36472d817227 100644 GIT binary patch literal 26165 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

  • jML#C)Z~u=x^Rd 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 Xx;GVnxVMQO1-aks)JcEQE|DP3B~t zBBG2LGymtKz4!Nh|M&mxcOQG_UF&)7`@W|0INF-`K z;=gp%_{sjY41xHcb>~!%9VM+0|A{M3xlbZ(C#f7gtabJANT-XbmU-Lk?=4@B@1Uao zqH_8)(#X=_k z={Ocb+II z_*rJA{MGR|fl4VUDe35pS=&61&{+9_&n)NV2Q*z~FW={l+PS0dgzRaCZ|go+R?@Fu zFUB4$P}%wVismyd>)hA&-6i7U;!Ez+Z5ldci>j0*9u>2gnA=z5xB}k!GnR3mIC0{F zezw-As1v=<}xz_Fa}uDjJEmKio)Cy?B^! z3$J2XS7UQCx0K`2g!&5>7RIkBd$(^_NY|lVx9)X8!BXSd<^5(iW>Pb|o~2!y$o%4_ z5;@fCD6;+AP}Rv}A1v&6X#@gz)(&f3xpL+3;p+mG?7_)Kh0gq8Cz3QaZaY}_Wb5o( ztK7ER$AX_%SGuo!4pUZl4GW!O;7#eaD-Me_yAgW%B6F5-Qc{xp>}Z&|Yj=sqt-#p( zyr)Jx3bM~|e|crwzOwN9L&fBejV(DBpQz7Lj5@^yb$6(Y$sBx_UvQU9bj)e!=tx}n$QX8lW%K6V(XOif-6qnM!MaL%R=b7e1vA_BuK0#4 z&ofE}f4Nn)O6jI<)O;4syL%&>h-p)renZMB271w7GwfxKe2Qfj+Pb>j2OSi``>U!> zva+(4SJdt}ax3DRj;^+LSah^U?>ql)ua#ntg_&aa*-aZa+BRoI_nh*Y>wmagHGCH@ zugg$Vbxt>Z#9Ae~mPplyLL*(pXT$r8ie%;G<#P={gJ1w- z);YN2%JR}n0ZU6u8ENVHg@shD=ic5VnvAKmw6vh0pliMUyrZv&gzELZ<)6?|4LQwC z^uK-k)~2asYN+`XC1cOFLvB-Z1FB>{CQ@$b!VlcnOTouUPru6>Jddnx0&I+(ho_u%zFAh-AAsltJgPe#-}%Dm@I7G zwr#klXNUAd(+_tz&t{oYl`HxwF;V|;oNi;~<>iI7-`u3#=P(keVB$H~-;{nMX28(*E%dJs{OH)(^1;%(eZRt4OaPoT!&ls zm$yCq_RZAu?kg#LUm$?ds^C_xaG~?a^JmZgp`$xJTw~*l9V3sFS@rk#)6mf9KB$=Ra`3@k^r026{6U<{b`e4ANg^Wz~&GF&G;gJLk_RJ%E4J#>#t*wB>eK(g!}F zyDtAAJbbjXa28g5^5jXhj0-usvgF!yqi(F%va7W$4`Mg@tQtYfmBP(9%v0M^f(>@ToZMlSSV=R-}{K7!wl{@APDU z`zzapsfJTq$!|OO6k7zGjoZE7y*tXwn}RABu!;JtGSk~8!;)(SSy^{;g zw7O-B>&V#H*zmA`H2X<^vmI3dJU-LUo<99pTPta}IQ9GYKMV}Isf`GE^O3-fswYlZ zHzZdVxlY318}Sf=ox0y?d`eAfxu1F?T48Zi{MA`I+rFd56b`ZKF(P_RE)#qoZ40gMY}lPF$c+DCf@gR!epZNYh`UQlbldP5Jo+ zZeXroQ(HSbJ*}PEXq_9G{#m5y4yR1lwaMz79I=dTanpQ?dWwjU^B?YP!WECcJuGI$H9X!(XX$(q!?>ig@t|%ywCfWF^ zvH4e*T0>)EYUCe_kd^#6^4$hNP<*LZg0 z=FLO0vPTD~gWhK`3Mw$Q?TuE?^IGxL56{ldj(+@jgNx@TF4>4&;Vfqvm(gqAgC%iB zb9{;mj?*}z5*EM?VPRpa;Q~yYYn!g~ zryoI_GBYI$qg$eBbi8?T^f%g=I66AQEJ!9NlFp6L55N1_kTOv9DBH3BbBV`7Kn-g#tdsAtwaW z-+-`!6Nvfq>^TJgVr{aQo2>Ep}CVPXBl!{AUZV`UVb(s`0+ql#~>lYdW&= zwUONBFAWW?dqYys_?-J20DPjNqB1h=_4W1L-6lyIG;7ypO|pxm_Vx7v6$qYvb&|r! zN2g?U=Ap2Okbr=g>>Scix=w1r={0&mW_kxLsv=&T|3Dmn9Gzg#d~`D~Ap#F)I)k{* zILs6YXSNAePH41Vvt~`J>qb5tlwmNB*#2~#vwC_F_wMbed>InLU}HiuKcS*haP>D& zx#Dr=C(lr+_6Q4){mKBxQQ(9UJxa@c?KFk# zcbKWmTRo+{)T>mWvbD8UU0oeT>0e|1nTWk%j-#Jd{)jbgxFli5wOaW2~VuF;OWjDv%@TY~b_X0)ixyNhPCu_NlkTg~Se>s%2KcsIJ4RP{kb;N+=OvlA1W zqiKGadnoA{8F_pwqtXwDD;gOZZeW$ay^RSz`aWgchuKYWtHpXOe z{N{_&MwkD9-|@aR^6VhXMjO{6W*3@$uyih=Hc-@({QLSr>1hB z(^!|Bhn?O-FUO@e)?9;5#G^$rHOUDjAWG8S*ve`!Aw-T}n*L;Ffs&q%j`Kj=VSme7 zxu&A#6e;QD_GaB|`|gci~hzxo;w$xoA&hR_edc{!0i+X+Z@69DMuCS4-7rjSyJ%F^t7}^ zoGY-^KVm~)tnxN-s=pd+2;BYb7_z3@J`&%+(G+FB~-BLb! ztvg<-d-l#9vjpYbZ^5KE2jG9W*jBPZAr9}z{XNB&iwtsKRleM|vtw|)zn{dx!ptmo z?j7AN-=++GrGw7HE%cl+cA^sr-3|_eZtb@GVb5t^kjnUFm!=v}SGhg^JQ>_>Re)2% z3VsE1Sh`((Aim%fqVnZ{%dGgyvPbj%7jxUn;h;280JN5IF?C-Vm`{>FxEiEsMWm{Am03^m8zKL|oWT4@*ks=M6 zXrbG@GTgQ;=NU@c6gNpm_w?zz5fKsL;Y}~jAFJ1|GgPjk%N*$K{gQBuftx!v=8*fB z8zd1FXZNvUm&WJX)6>)O*>MF1+CHJXz)Iv@Y-nSkB1QfFs(I??^gM&y9|a1d7%S&G zQu5biwJ4=PvTf(<+^j5A3NdYUJeryQ# zAj_fWt&h*zTfQfhZ&P`1-3Uxu6|sK0JAS1^JX0bm#p)!-Ywtw z(>|o_UFffo*1Fw>1nUnEGl?pa7;8lY7H8V+5p`TJ&}pBUk#=E&9KQYgf8$-O(G?{Q zcuW$Tm{nc=Ow)Ton|c08ZR6xX)km^!!~mR zUo-zbGh>`%bw)#DbGc%rDULiq)T%D-HkX^9*%Nc(!Mh_5UOtD;G4E0RRD zI(@^7bxiTJ5ZMVj{5`ImR3?pf5S-2?`=Z~s2DiVuMVObiJj-FACK?oBe0B@7g%8P8 zT=vkRheAfLJ(t{W`R*Yeo2CMuhugL99?L!*5dkq4b$rTv&&h~7gxD?LWJTC63~niU z-~byra_vK9s&X)8^=k0Pw7&eHLtFyOyk;sz;*n*?Ri|u$Hq}3;NnJ_Ag{G zs%5_pO?=#mGL>086FInD$Deo`F!N|zZlIYNy+if}Qp=57y!2bi?19*Bi(k84p)CkB zS$!)3VjD7Co_~7KFc;7NN{zp%w?%pb>C$a8y@>nw%d@iP2sc#g`J{NBu)^8$tU5lz zvDXE2%a=F})U3?adL5;GLB0AjM0Hh^sj(`V0^qLm7W*9daRdtSq_vymkTN8^xm83o4ZNJ|r_Kg=AU znm{uB4!f9K`(5$%tJW=FNy09dy3s%r9#A^QXu|z*~#g=UW@lu?0Dxb-;Km>qS+NJRg*2vTN7N}vxOo;75)1CR6G)%rW@?xr=Q z#So88^iufTvIt%Yx@2j*oB?*-gv3zBy`lVVFo$H!!OMyiGJOxe{%4E4eCadIl(yQe zyYP;01^|^N=qdpkm1tkA#YuT{M4&fYURoG2(>s2fiewiD%GCM5>dW)@Od)}?UXqBh0@S#GR!#hAkAM7-1LB(ej}etrzaE3eyg zOLT<*l8wR^Am8Luu_fm!)<ca)#cPD97ASHM<;;?64^{4`>2u<%D^Df(iYBl z?}#sE14038y?kM|1EEK%?jIPKtrD$mX<6M<868T{>**$fMLbHoe=E+_^l@vp<;wg} zri7?KA3U<`U`9p;i~$Qb;EhU_`>C*VQo{6bs}soS7cX7_d&Fnw5P3&IHHdBhxo>6H zhnh24ITLeXY?6T$2+-1{nRe`EpY-um17zHcyA1@J*Ft;ugMT@<&m&RG7&#Av z>*b1sK~x=_oiQx=iAP|Cy7l_YKEw(YiREBybgm*`pKgX<%>*s+E-4To%gf6F?5nP1 zdS+$5xfBDNS9YEJ(jQldaMybP&ZFt9{oV^i{E-CI&Xg+(1yhsO(lIbFfF6Fk>OTrW zTSYDS;QaW=yVD=D?jjYD%IRJ>uF$-0@S*$P?s7O2)UU$gGA zuOZR>=FFA?qpEAiXuW|H zCqU*}%|3j0cHj1`!e;``fBKs(Hj8hd7d9!m)Fj-CfIQ8Ip$$8GP5YjrVBuMr0}nWE@L;+1dQ}sOWNA(yq0(Bs;~>Gl&7Y$ zs%T8@%iI7gT3fP^0pC^g+(T^b3ZJlWspP?{6Rp{?hRrv0hxVw9>1qe><$o=CFkedE z>-jkDfVLnks%~OD|5lahb@>BZyLpg0fO3iQT z6uNx4Q&m;<-efh5X?n>*K?RN#6%l#)$2Crxn|II)i!C1@&YFUDzk8wK6a&Y~`(sn{ zoisG8LWWj-;snm*GjeuzK8^$$?3EC;>zR>}%Sc<}M;jswJs1TP^prNL>>5gGU}0sI zoS2n*JIz(aQ@FYZD)s& z`1I$%(5blF$1)2?4W@bn|8?pFbMmKFX6*6rY0josBTc2At*oKUgFX9AKR6455@Wi4Y%iwO^R9y$+*w-Jm>$G61s(#OPh_5G|&e1jTXzc7<0;rK}7!UGPNg03EC!M#{51~`n)lxd{uP}-UteKD}%C)0}ZChVY&;HB3N9zwSua@f_fc|Z{Ho7oj{H8OD^J__Eo7H1v z3p|%F!@VjjEKJnA0c_JErP2EQxpU~qn=(!0>-94#h-&Z5A#o`mI0Q7Zp!CO$z#*i` z`NhfF5UxYgPCri8AGQl3>UU5;KtLDx!;Ksg`LACG)Ht3e%=<&b*q94x1Gv$y^R<7? z#6zMj5uqr^3KZuNM%eJgtC86*B zIET(syaKhpWsmx?Dwu}*4 zBP8;N{@i%kUixMz#7$pz$X<*?drEd?XvJ$v9+ zF+bb$&}Q#hJ!&2FGW#{&yLtktf&DQ@40rWatIeEGxIL#K!2jY6tl%F2NV#& zJPdA6{;*~mpzPzzB7`0Wj!{MH3D{T{@#V`Gq!B@boR978XDQ@tqVfNOj!OtD_veja zTV<4gyv1k7A8Kf5VEQiwilY8-l^@}(k~5f2v9hp0X@Qj+cL=N@iV|<1kPKm1>D!oFvR@j95Kz;7r2ejDunKO z_Ue^{kkDxTF>X%Tt675rWT+5GyTAjw_Ol2JXCCAB(4sJ^e!uQU^3K75U=n~ck&*3M zZK?~As;8!=!uIL9UA^j8!$Tt4{tRtx?PBzzwX%~Z(MBm*GTlTTF7wS4b z_z^)nv}B%=n@dbfyYxP2Yy3^WcZlBMuRwDMpz^*xw9uCI`bT?-ZaD$Ynx^;S9Q+W& zT$X4h5nxpBPH3A>9Y3CrxefK2HF#(U@z7}S2}9HKD);yJZ;Gdzd8x@lGTmM|0fM;U z%a?NRe$(_FN|3xbPa&M6eaJ(iGacF0p*#eNZundj(s12%9}EbKqm1^2lk#mkAD5H zEztpXLT^*A|G*E&-g2PWxD<2E@$vDjERlMB<^bY=!i&+Mp{AUYU&l5S5 zhr0+eqk^B0ujQ5PS!?U(2?+^<0-q~<)ks-tmbEcPm>PqH_P}h%@=Q)R>y9;`h0?%$ z15f|6We>vVIE9>K<9dJpc0A?;rZO`#AUkfOv!xb{Wg;4L^ENOzhYue%Fkt@>_QR^N z`oQ9;4WtuJ2s79gjGv2(OTB*D8pKZ)CkhiFSi$tnRO1t z6)2;hKYuEK9Sw{fXwG3G9V0%SL%91a=v*Hee4p-z(UCBA+e?Y@1<+#W%} z;3Iw)AxR^pDPSXtif>`lRlP`HI>;P_pZ_WS2d(P1gFIx!v7LUMlaqs#4I&D(LOT&Y z%(fs1<(H;35&L*Yph+x=)DOvG3#!Ha&`|I_N|rD5(<7N8MQBOi*#8={7Kku5HWph- zLko)ZRdqD8nRoYYOo}_4Y7c=CJUEI%wtF5dN^P{vY^~R_Cf@gfc;5i>@%(UhE%M#t z#|F#x7sUvicRGA>pq6*nE&;u)I8bDG0u7PoX`ZE~o_%qyqUFVT%%QLnRqZ zliUmpA^ux<12&yIMp#4xQ#FJ?=GvlHU+$8qL__#k*46R9*|UY!0=#dGPHM2f|04-o zgHq2WoU;r83VV0+@-jeChk-}@&6b^j|7#bB^1n^$fK3y4v|(mRJn*=Zk`giRy|t0U zkWiJq(1eW?BL2&qLrrQF@&b~fHyf}J=1KrUoBsKSDY_zqsG)wp-*ec1ZE8ZYP@*up ze{jNb6zuFq8RQr5La5=dc#z0YNnzpPV0a0P+||_;pG|ZSruwTT!vf?V!~o3EG@%5G zCm!7(YqA+%l;71~v(Bomk z$+KGe`&T~u&Jj6MRN8C#8axpr8|f>*c1hE(;-&~yTU%S8#Ul)d`o_kDrRkS9BRYP( zyx2hSoX9wSn{K=p!Q!*g`B{SBLsnUNu62+w;!iYWDM?8L5O;G6sJXGn0SZcsOa=Y>r=pU-%6Kkfy@DiB#+6FvZOzK`gC*MMs{&) zM3WU6vgYophs!|bl|QQ6=ldU)8ySA>@9#tzj|Cp!m>JQCnYL#kibyYKHdyo=y z;L34Q)+S6t+HxEzct&{>ja)wuj33)`>4W#l=(erPr6)0pFlQ!0e&!M$^+5TObGUnBBzL zSNU)$Xt6FKl=;_)$Hb!R@*|SzB=ipO%;IZv6RTMDMz~elQ|Ga+PPNd*pq09)-OsX( zOI^mg!ZDK`ifi~u^eCrdPSP7>z7H55i1p&qFzJ3-%lW(+Qqx*NcVUuV;DZMb&^q4X zkW{BI<`|xjGKX}nK|9s9b-+3OIYzn{TivP3_^*M1Lztou!I(^&eV?MFWM0FaI~umO z^m2KhKS2VNp7e~Q@beDWp?MIQnflqua2m|cwAl;A+S1SFX$l%KQ9f4?8eihQe1>@1 z$&)9ww3yv@+bWz_V%xUuvA(9J<>Kuol`kP-VJJff;q=7(^Z@35=#p*&@87;1SVO(V zDviz^DJAt-Nc4NJL1&d;t;GxAHqd;KAqaRw%l%l+<8{(MR0{e;S3!X5XlWq!2<0tN z=3TA}BM-cmR5j1kFLHBzRn_oI85mZJ+9S7||8J4W)~#F7^w6&g{1z2hNkiItdimCk z&jGep;hHJC8Z&HE{OgWP%wqq;r>}n)^K^b2{Il#$CABbPw3J;801Ns319Pc;`vCf( z!Uej3y(St;NNFDd{&w!%`A|Urz}2iX9C>k8YQmjzo{Lw<>z-s9zMe7_l^dm3iEJ$? z{*e;PzI8jdyr-OEGH^GaCjU)iX=!^#MzXtY67E2o%U{5Zt zLv9wXb2lO*OMj)ZtDK-Xwhi`A&$)EZEi6b56hg4LL02vC-)b}T{jP)5-Ura6{uSsr z?5%t+HhbIkRJ64C$e5S>a>FTMEv=yYiFb6D>ZMfL_*GJ6W$m~mhI1L@tlnsD0jJYe z?Ct{j3eqG7f|uBKySW*!tSKXHfB!GZTzL2oLS##SGOa+#3gt`w(#aF^J4CZn0##=v zY1~~x9D|}%C;6}5ve^nM`1iH7!aO`Y0s?7-9$-+$njqgmQ{><)^6MSUnw&aU#2WJp+n0JylgR<-Px3-^)&{ey$2XJ&A~uLie^ zuqLsZtyxt?nFT9nI}NC#k=(W|P2<_E|B}>9gAW-)>xF_@Q-ktbW|)>tm+1^Z*U@1Z ztn4(=r&7L#4ZYjq`SoQv(DeO*UI=9qJR16^hZ|UoTQAw9E4Rzj~<1tIm(llWSCM?;)&FU zT6YcOVMt*QpM`go=In`QCCIoNCVSbLox>JV!*y>}8p?L(oirlNBB$`zJq z8nXGAQcIj!cV3Tk1LEdCD$labceY7QU@VVWZ+P^3XFr~z@E)oS`r$bA{*e(*E-pEY zB9uKX*}QX8pFWKMq!g`{-9jc~<%Ggaqr3#l;7bszgq^4uN%kH=KS;>>F)sr(h?8Jr zHp^zSldS&uvCcYoRTobzzkM9!KJMvYcK-5Bx7`_^_eiJvWfM&xJ7Q)|)JeU0$k$~F6c{i*s;SU|D{=H_5LC4K6B~9g+6F) zXg9J08yt!CkOx0tsblsHs$tT0& zu|gah?u%dm)Df_ePt(zH0-_H(zL*BT`&w~cvG>@yw^)BWAA<8aH1@0fI+R8up5 zOy$obw(tKF1aGS^dMiMVX_94nSVJRp71^F%8&Icshg&`-Dk`qj)?h2L-Z3~7sm<2)SkEiR5z4fJs`Xy4v#ZE!N8&BUB zaN2(ZgXyZ8G^u&0b#`NpG88tGpyToYl_vT=;i>+7ib}frDHN8B6Zc(Y7`-bdT0U-h(D2ed3ksdUqIdbV-UR{a&*TQ z{;iCs3UC6iI*2(&em-W|$QyjDN>&vuvm3?YU%!@?mUe@|FiAyUaat^(jx2^%tZH`m z;Y@MiDl`-fEF>i~^@rmq%um!Vw#-I9dW04ZrfEq>@%BWY2aE?!9sBsnr`Y}u{~{Zt z2&O<_Ut=nmtu_VJ%@~!AwRd;NvS?;$o(WQ?Y_@<+#(n3#yNysSf;aN6f?V(N^C<%J75HqABTcRh(dWPmy{6%%`8cc_g4 zQ6YlVxyAD?7I5yb`Zqe-NLM#SClw~^Pkr{x6?*egv?9O1e*n-JI>OW|ymiK-@};4U zjzxaw1CQ?0$soeOFbf;K1{b4;DWaMT1k?~gfBbmxa__q=vvMtdmb@rMDbK~?IXK$Etpo2uIXr|r5@LvDOLHPaBckmjef2#+CPP^})Rg{xolp*%hz8LPyU2U@ zo~-JKwTb<_%^~RJKY#vw{rYuPDo>q?%Ns!$U~XXe$${L^_$w|h4p+4TYG{TCNP=i~ zo%{i`fpa{o!1Q+Wzpv%Igc-3!+=IbX6C3h??_tEi$j0^u!U=qu<1o|P_zeps=K5WQ zF80tYAbr7=6E1b+|Av+klE(wzEhE+|(e2OZn(a(KD;aj;O0$s*hU1zPAwC1l9NdUM zuQ4!$%Ab>Y$6AmCGL*r)JwJ#G70-w)b_J}xUky!#6r2!s>Do2fp>Jje%sU<^W;S!n zUDHdbN5mu~C+|6UP!EY^>j59`@SD3-4ALbq6Tf$iYwO=D1mgI{An1ND&44JYIqF_S z#9&{a7-q5chfQzdl7|5pS9SF}0+q1nt#-B6S=-wse|iHmOP8hp&62ik4`6KSnfK^^ z^X3h-1n_MOT`jE~fP6R`WwrZl*%QF1qx|13L9yEr?)x_^JmLWwPU>7vWf z698>v9sqqWajhnq4K8D0fnm-%8k!el?QZ=)e(Zr(GbP2{-JP%*oIwJs!i^tXKGH}f zE`y=r#HAhme|0RES`X;#E2GA}oqVwBqZpI?^60kCg&Y`GTO=rWPR>JM?V#KOOV&aY zz&I2C|0rSvxi;`N7sTFKh!rn<+Kf{c#zUi#8DItU_u9|2b}9!AHl{utG`zb@r44`- z_gwEsMt*KvtG7XkZlTek@S=r9GS+U^Etwa!o&vxK+BUWf6)+2uoJU zeVQ3Rp0$ywJSe~^;bByVT!ZQo`-H@ll!p%<6fI6vE()(B zE})66MxfBXeMkkLOUB-tU$}sNRtniH*o6^~IXMgo7@R(9%pK*JKa5{Y4C%4?pzA~* zh$>cY?%LhvbjOHWbpIpktzJaB*j-XQXxIyG8ZmKbrsX^oG04nubX1doTexEN7xHI~ z$p{Ek#c;D6KYrZd%9Y?dcW|BP?k?PP+y%(|Qedntx3axmMphPJtp_(RaZfE?Zeci^ zVDbJPS_;q&D)_-U0OWzcaY0etPVx}$fd&efTAeIWFES|6(ILT$?c8}JwQ=3L zbO`B%M#~GtjwI%Gk3=!dID*wjV;y-xuLm*Xw zN;Jp-lBbRy^$8nY2sfA7y?gheL!GP4_%~!IQ=0#s^)f8>3JOL;%`?$o3GfX)ufrOA zQVp4P6Q}fr{DSxVGaf^f*FXr66NM11KKa{w7?~fPb@^=u+%)5qaYkleOG~?Nk}3%N zF$RRf#zlG*#&5!Be7qYqhfBuL&wHQ-0JBOsjqSUHGSPBpwKI8)tEKH*~F?|?Hg$8cM{z~1DbX$6%= z8ps2*@?2bYZ-r^9uH(D}l|c+jg7`i;n1RP_K{aJR*{sj4y8-Zni%$Gx?2 zIwCt;IaQl>t$^xDt<=W-`}d>sat97kn>JZ1!VTx42j?d*ZxY^*o(fYH)FND#Lidv( zmza@Z_wxo#6ga)74Tp?$U8M5e@bEuQ zC*qJYOihTIifgx0QsQ3SE?2Hh?@TzRJJve69JWu+Yw1AxE;T2LY+XvNv;NHQi+#S{ z8n_UI zm0~vv|A93A9Fnkfk}SFZ0@3VT&m3%r7`nA1~?rAaP$~qd|JpyX&O;m`sa3 z0U#EPi4$?VK7mPFWL>(`K!c#$cUP5(OYPyYG2U;9pNbshJ8sty>ZRLv{L5XcH78?e zMR5OFVGGydLbv?%)TK8Z@>iTVBwb&Anj|GxeBVNfJdGP&b+z{2e>S*moyK1C9LjS@Hs);pU9!&v=8|;+4HzviVtmq4 zl+<2ZnBTXNW5(85qV(O`eu!Sh0@YL8FH}W*?&u(l*Y`kow~A;-$EH8?o>9@&wIXl$ zL9Z%g1D;>|y)^rLtLQp#i&D}trK2>uyDnzmJmST`aGRFvNW7^%ax*I$(A=c&R4ueqF@!*kp2#>JWY`vb>Nl*{Od-`;>W69A4^{qaHfnY6q- zFo4Ct-Ni*k#0|o~zfA({vbPb|N^*vgNiE_a5cybq8SLC literal 0 HcmV?d00001 diff --git a/tests/drawing/cairo/baseline_images/graph_directed.png b/tests/drawing/cairo/baseline_images/graph_directed.png new file mode 100644 index 0000000000000000000000000000000000000000..c85c3b668ad66ff9355f7e9d467ecfa5019c9c6a GIT binary patch literal 17911 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 literal 0 HcmV?d00001 diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png new file mode 100644 index 0000000000000000000000000000000000000000..c85c3b668ad66ff9355f7e9d467ecfa5019c9c6a GIT binary patch literal 17911 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 literal 0 HcmV?d00001 diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png new file mode 100644 index 0000000000000000000000000000000000000000..51608e9245d547b872aedea2c1cbae494eaaeab4 GIT binary patch literal 15413 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 H*&F`{h$tl;(%lUT(k%$m-SwTp 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 lRKjZ%!T8NAdR>!t&&CO@NYvvD_UuUiOz1;gw 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 literal 0 HcmV?d00001 diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_basic.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_basic.png new file mode 100644 index 0000000000000000000000000000000000000000..551d858ccd9fdded438742b1690c0039977a0af4 GIT binary patch literal 25084 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 literal 0 HcmV?d00001 diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_edit_children.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_edit_children.png new file mode 100644 index 0000000000000000000000000000000000000000..6763b66929d395f778bb8fb7bc36772b8869dd3a GIT binary patch literal 24552 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 literal 0 HcmV?d00001 diff --git a/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_directed.png b/tests/drawing/matplotlib/baseline_images/test_graph/graph_mark_groups_directed.png new file mode 100644 index 0000000000000000000000000000000000000000..41279222155db204ff509001afff978525b6a811 GIT binary patch literal 26545 zcmeFZ`8$?v7dCt$i3&-I4AF(ikfBVWL2)6~g`#9krI0C8rbZ1SQz)TKDP;_WC`lwl zr3@hnm04td_e0&!_x|wx0pAbbvu$nn{oI|`c^<<$*4o#;_H|s|W1zj1jgO5&p)958 zXwfMYrZp4_qsgL$_%Aoz&pP2BDsDSW+zd}1ar3Zpv8C*?a&xjj>1JV=A_F}+3Gm0TD*kSNypTcLgBC?e;Dp*rrJ>`M+IqG zTa7$p`&vDsjk-FXpTyQ)>j*ZrziuN{e9t}7N! zqte2t5=&ofWx0-ji%^kT6NZ1*zhV?2|J~vL|G)p=UyZwD@6ysuA0J;)t3J*jE@tp8 zalGy5JEgYkI#kL%zRPtY{3%C7wz`XK-6v1m%zbwQxAk`A>TQ;$Qa4mTnm<0#@XSWr zK(&k8`}dD>zp1{Jbva>4ql-ktD-YKRQ)!ZYJkkeVzP!9V=FezD(a^_(FPF2bYK!m- zgfy_do)~(-z&ROdGefCn8TOx^JgHp0e#=&ot%ds@_IOS9j{f*?bvwJt`8_?2!D2Mq ztIS$qNdnO-u44I1glfX)COAy*a2`5ietCgwH4%PRY zUIv7g4(_?MHbnWX`0R+#SksA&1RGktOjdwSn0(XIQ$Kq?B(h|b3@ywT4>x<0VNou5 z>?Fg{9f?{$-gjJ(Fzl(jv%M~qYL{xdg-YXnkePYi&_E-`!@@VRa!C2mS@zt;&zKE@ zwM_OEZW{0#WY~36kySN}+Q;2UUQZVZZLimp>G)vR;F%H>$q}%#^y-ju&aE*<^DE^hPmV29H`pBd`)4DI02VY7 zTN&XT=lPZO)wR_Xw@U}kvgZUIpww1$R{!Z)`)H;i{%r7CT1nN$HDO6X+0LCi59A+v z&Y*W=P+!HHwP@uZn{_V(h5TNhVG&&A#!qXK(I%7XdQ{l|wOLee)xqE@meQ^*KXuM) zD76H{OY2I$aw`*bUdgs^aXnAX|dN2)D* z>yx)ugeEy;ljVAmZTser{YR;%%mPX01;#rw3{wr3Dh5&=Lddk2rR=)3aC&rManXGa zw<|kihLubGup|J@)nL){5)@y;sS?cZY!#k6Tdg zF=n4aYv#*ZB0cNEMi;|K={rsGk1RDl#~vzX9B^2^66<;{ScFz$NhW??|7o9bPlbg5 zL#PqoyaC)f{`*HdhpN`i^r@jnN#pwqZf+0Do8Q1iOZhfhZ&l>OM@%Tcj8oY=GG#}? zbcOuoX_#47h2R_UtW|By0@S4`S|a?-4~yD{wCJX`^PlMqRtu(XknUL{`s;LiqGzmQ zg^-XNH?2u*G34bDZS31yN(v{xY);u7 zUxQN`v+)Gag-Yt~3)@=a^y%t70ixeR_gu=8KA2srF|~l~MJ4t^awXg1#W89gj`y9a zesmA$3+EUjN({ ze%c+si%|%~r%!s!sh;{#mt~_(2B0uEt27cuyO|_+lBP$wy>{8%se#(lLOxs8aZi4z z(4ljZt&+Fue07a{79$GkZAc}fx6buJ60?`1?zH*1c?ce8TQ?I`5(V zkG{Qg8Rh!3!AW@VRg#e(Gc0P;O>^jL`6nhOUUm-k^|vK#J!kZ} z2aaqhpW>(fXX9=Dno)<`c{vzXMr;{#yT!!Md)di8!+i6^dqZM-0>t)oFP!@MQO@R7 zkWt5<>5*C*m8-ecf1r3=M*hX#62F<-8vEsAzFm1E+x?tH(9%VoHvZLt9DqD1u>K4^83aQ zt^_$rnH4w%PANGw<-T5|{qVr!E9wmwm#$XskG6@Pd#9~`x#Dxlr`kO(`xuH}SRL8- zu&Q~WSo_u{hqM2=8p&mC{&T*e%YtP~rd1I5rbr*hW}BVrirhT?daW5_3+sZYBU53e7H3+VqGLBXL~rnte|vM|V$~be zJ}%h0UB1+zr@1;ECfSE8-N%;(mVNv7ja!1>_`VU%rpY2oU+l@;?9`Kvr$6uQH+kJU zq^urL@1k_gbjyHKbkuUzM^7e?zq%&0yQ?blT(Bsuuo|X5X}1JI9$xy)XTp6v zWM^XT@eh*5$}lhOWZlU6XwPwJlgxlOGS8y-w|Smp5?OZfW#_0!frz|7fhhM^%}VOE z)f>M)n;J+xJ)Z5;DnpBr;d7(V=)K|kViP|eWL)769w?dfE1WuNYx@o^%BbU5L(0+> z>-Xu~eEr%`#9l6%uPkBy1F~l9UT6auY!2Lb#*c9 zd9Y8~K)GXqfod<6Zc)z=Y|pcg_G zemObW$HK$QDvQ0}J=ibHuJOn6MaST#+FMak6%|7MMQ=49VA98plD3{(SUwXWn)b7Dzyd-KqOxjz+} z?ghJ*`M;MkaePY(H*hi1qNg2Z2u*Ua1l;-7-JQBq`N-RwpTF5hF0X5&kTyv;D0gmdg)zlHDqo>|78W3S5= zfrwlcaP}6oE~uKSeF{-KkL5_*QS)(cC^3}8gtXGT$f+E zkDq*hZ|{A3w;%5-l`ix4pIt0tSs5l;?$_d^tlwR8dtzq5e|K}9!_}ETVGG)Kw2DeB z{g`fA7R0N7C}iO?#H=-?3FO#n{fH{S3=+i)-zQDd+XZ_qAXycJJ=) z$PzjjmWSv=XT{kz4tYxDPq zZL_EL^#$!&rxhi0sNh?e-my=aR&vSVNdh?mI`xhwS=Kv_yV*>R_WEG6np>6l*>l?b zrjo062C@s>+xPGQU&H;PH1lFr*!UVvP5=CmEVyFV&$dzU3JDb7^^e-?wsT0IF$x>~iI6#}8CFTv zyCFr}m3mXdSM^PlY+L{AyJH9j#~L3AM6Ta&U}J52m$(U>PM_3qU_kLr zk)-<2%jMIZLUVu0$MelU-`|_KbH}riWA8*rEI|Iv-9Qit=j3;-mAV(zIZ)UQu-y&Ie1u;!Y;_B9LT~3nAGTMxN zI~wODhUoM)`;AN|97J^C_HAihc@sU!O=WuXmFuxsZsL@xLi*E6Y z7u&wSy%ojR0YJJ5A8G2J#dG@CoN#O}i|x9}$sX7cZ6djvN6KW2qoZRem;CYf>txqT zr)eS3MUwpoh7~tfh6CGfWz~Vm3V#n&1i!kR?Ls|%TKMeG*Tq<7!=Fi5%8J*aYo&$5 zbc2|+=1Z>?YmaP-2ZeB3eg4+&eD76%$t&*q!~|)@MQ4^E|IiP zWXJ^sp~BG`bwq3WYh2c{hOfhvHa>lqW+?unF(dv}F8%vW{FRrN7xqxJ3fA@Mp=r)V zUWFq`Y!xdle4YpUz5i7>@N7ql(T9fzm9E)|&a@z2^fqP){rvclDX-<=VYqAjwj4y1 zUC2Z8;-`Qe1x_98i?th4b|KI(-Bvwy7n%3_M~BqN&u>mo`wR~c(?yeDutBTj&R$uu zeqpRW-3#ec?i(Bygq4QW-A8c*;8VES-eJ=3d**!#c6;)LZj&@w09jetQ--xO<8AQ= z9iNnQ;oF$-%zYpapg#-$ncwqZ@K=7DA5h3M-_!7baj$a2uDmJo8tK94S8uYXNC}5s z31rr)mz6r0myXjk)_fAqP?=v$Ts&2$W6+?AJ|XAPZ#F;LZ-}jj{|t5*-PAw z70%sZ;%>)2AFYm-I|mQv=rQ-}mwi*7!!1dEoa(Wq7pazdti5d|nyZx@7qqvZ*K-@B*3fxZhE$klT5d(u`7QIRnGO%#lz%cFC;MO?b^ZGF*Di}jp2AT)TpGY& z-;~Rmd0wY!BM)a<&}9I!*@UbQzwjq4DwYIJO&4>^r`!C(j62)L0o=q*l(6w^ICJx-St( zS1CJL-ak)hp(VWXFJTbKs^}7i0FBsj`URV@U!T)fjysT?3r7~Hn`{2G)5UHfcAQ(d zRCgv;^;c$DL2t?}*aaaVn9~yy0-m^b^GNVtxNrd|1KXOG{SKKcpOT~gI&NJXYntSb zoLUW=!S?5AT*J58rZ3)dd|LV2c0OlL{tUl>xf&q#g)je``hq-4NijSHC;Io)r@f!} zl~cP)@fat(3x(hBPsb9$&7Uyyl^8v1bEXv&rU;Aj8u`8~q$0m?&ir(r@!|7{R%I7? z*lD&e#N&Nl>{4otkQCPDcV2Lgg+q+*8aYjN)(n-L7_}7g6msB3Mq+TxHKzxe@E^H( zc(^WW-P?1AafPIjC16Lg@q>Mc9oNIdx8t;)4K6X{ku|SgEL&7+y#LUOW5M8(6yQeQ zUz_`08^2p56cGO!-$qHgGC_%oDUnHEe;6eVYv~91crA9JXK&CtH>r{}E^9>iwFxxQq(DPr;n^ z#s$@Tzzp^^Jzj4d|5`kp7U%VycXlYme-AdaZFWrQ=Ef5@@!S=5LfTuKALXeoV2jEz$tDf7WUlpI*d|lx~dv2{NECaRN0C#qj*;JmW>?yrJX8b zJc%2xk96*5YH$2ewy@oGjZid)&w?WE%b7BQsh^)s)&C4q==q!@t}W#2mJsE6=!uJH zXGKW&ISvgS-+@1!A>v9(`7djpc>g|rq&hlOk{@yLu=ZTgO+~w~`7C*z!dl(1Tyc%b zp0yX3@LIEmf=IgVVmUiK(V}X-U~alwlT|dFe>vMbh0p}?x`I`;hLoa(`U}K%B>F7B z`tEqi%&N`ytGZ)d4Vo;BzW7Z!0N%30Rsu!ZYOAWghH48+_uc&<<5^SxGo)!VW6c>Dg?aI$?fh-wVrTfG1zsgsAT@NYLakHh@8VE%A`PCp;!%e(dL30``^b3>?Q ze8P5z?OX*bdseeIWS=@ArXQz|$T!{-4a{o={02tsbd+G@y}fG%J=+;>NSV?PpSJ-P zZ1&aPw{PG5c~k?CvnP$7tX?HcYx;_4*^O{u5P2PobpuetbX(FA8wLp`aCFMuzj9)f z?YPPUo^MfZ*DpCD=5$=KYCKX>Zs_w-mC2q&kDtn5f8@toTsaUsKRvAQ`1$dggkg&P zcx_I0_Aw+k$ImAMgdsp4cojiSoAv@1Rxt7X*5z_*id$b(WJ`;g-ld4GQfrdUz;PF~ z&Bd|q)~F1>ydZUfz_FF-3k^5|C+^Wjzj#hLz_sT#gdEB_vecvX)XI}ZL$3sFk9|z% z#j(O>;U}#><=4_**`Y07D6A(cMa_U3^YqP;gug>bq^X}4)&_yhQu1jQ#rsePKd3QQX_CW3>!1^r9 z=Vz@oJ&mk7@5yz-^o#7ewn@p0-}8~bEl%V4SB|X zX*^Vccb0*f*FQk<52^67`(b1b*5v(oIJjWV8AX325PC^Ir8iC~g0)!}I{)5f+WgPk zpPw^K6eOmAv!@1H9LPfV%Gx!_bxa(aOOvC_t4^2R*%~OZlhz*X6umb*ecB41{v~%GN#56Pls=vU>4Yy^yEEyE_IQ8mlc86u1z1C?gt5dq zely+&Hl5fRHSny{qHECqt#;}2Ep5+yfkPNhd`zb`KglTg%2uQLGd=hA?y%ewUpzaP zIu`%_XvC_5B6bQRdRZU57sGLygi-xg_|O~LSlS5gdreC&!;`a(D>OE{l& z^WvL`gUNm+r%r=mEYDk03|e9H@awfQjuA=pU%wkIj(&R`n#Sc)PZM`W%6o8Ik_#2Y z-iqJTIkhfJBASO8oR$r~_GsI?XHThBpg>-kZ|9Kb{iNxld-gvYk;|pZbQegk-xNIX7|re4;XWc zMyeY;m$cn>hcli*;2b;s#nZqtYOu=1C{#>TuTaw>yt^XzuTnUoe_ZvWL~85{ln_o} zAj%c;dNIX!_o^-s5LxH#HSoq-=-Kr|3DKEBx4OoHd1YN@YS5`$m=o}-~7)3iSIFk@jEy*(Y1pP>tXR|gB z9(r_|fObhxq0*8qkb_pfeY?+e+VVa~GVAoDqjiKn_7w1{0+vaBC@mU-B z&Q?d>>^0mX%l(36DA|d7kAJv7KTKYi>7fclv3Mg^)J9XE3_P2H8Sz?UR?n`LM=y7~ zq!`V`=KQiF(D2T-3kiujVO7zHOK0G5QgvS&?p7}+5Jm5j(N2>sx-q|wMM!K7TxWHm z^u!&`)~6mEWVM5Sd?{8_`r>JGd`c7LRl?zsPgvp;!JEG>N{JX*Emh2tzMI;EOdCZj zn(Zg>P$o&|r>ENH^3Lnn1~4Z@sAOq#q+HnMv)t`E*XW`>8SW13%IyL3<0%L?P`$j` zu|W7SsHeGG@Is|rB9Ycu9m1m|9Jt1}gk4pKHHLLFC!5@{*PIAHbb zgCWLUm3g}EvB@ENFQ#!?1@UJ2M)7XUgxoPo`&BHnYm#H(chMBrqDD16>vZ$Fm-OeB;9v-{OEul4YnX(L* zmHFf2MKfPclafG>PFT_sIja{7W+xj_e$4i6u&~S;d7(X=ojaW3=3@7ywcoL*#A$N5Tib|QoX9N zcY2fdXwP%zEzQRw0$KAeRWr#@4g_v1I+)5nH#=V9^(}rb3D^#0dTQFeoEp`u$a^aUo`P0z4HDkIejFmc<3 zB+GK^rN{Y>nTi=imrel!T`_(@_y4$oFWFXP>qD)xY=D*&womdGPVC*s6JKG?J^N?> zTDA38_emdc`YMrHN9q{rqkC}rDYd#`$m4nvLxw>cq~v8h{UKSS+!@r)NaHPK3=`g$ zVg44(Pi1YbxTa?O#7Or@*^duCGavT*jC|P7$@cE9t=`!2PuBsb!tuY!<1GbQ7q*8P zInJD9eKPsufy&Hi!@0%ONLQZgOU7~70;l>5IRc)aV`)g%iwal{MUx+n-y#mpX{X~; zU>ZAe8w?h22sQHaT9LkFCHvgy{xzXG3!))QX^-`voiKBH&Yad)0w}&2i}9Q}tV(XN z&c!J&2Imd>VG)%$g+AmYz8_N`Z?Rzw)eT^#mR;C(=luX8asZ@MY+ok2<7>UTZ$~vf z>yA4et?X88Z9h!^3<|;N5f;O)`@%NiM@7TB2AVT& zpqHNXAYS)17;{E^ z#}VH_{e@~bjjA94S)WlvzB8H~aJV3T_j8w}m#DNt8LUs=<3=6{ zVU*}QKJ2$xsvKjaYPv-}77RVL?+HE=KRYV!J=()y@3l)n-CGg(`9S9LB>HO@0olhD zAx_HGKtXR15|`L}9TM1hoI9gr*1NPaslFV~#GVxeif0WhGeedV>sC)Iy}>2#@=ZtY z(x?Ji7ATBp2BtSw&g_o3zNfxWGTN(AeZ zRRyO?pgBI{^xvvRrau%-I+*W>5xTsQ&L2cgXhEz#+eMrT6=pqrXXRzBo`zHom=XV= zA*o@u&b)5aL(y-@gIE$kD-uvb(yULGbh)g5 z$HQpyu&u3}=b)wE$gMWYLEYC#wFo%*yfqdSFvgJh%p&G<%j%g!O5L#)!!A?zT zjqbIxs5a3gZ%OV;fBiRm{@IuzE-PEhb(dbrc-M%*nQVY<CjLPyzKsb$uvt#}jiTGPoo3JBDpEUI@F~S=Xi0*Cc^e}3B4C00<*#WP~5sY-j0IiV8!nH080NY=O z7ec|Y%1Pc<}Ft66P3)#uLkkO)W za1jh8N9Or^-~MgR0$7esMQ}1GcZv@{=MBykYJW!;M$a?e2Iqzh_$1bN_{HA|CyAhJ z{`^yxjU6Z|3g~nJGDbvNGDb^=y~pPrs4^U=q%=$$)DvT-bvfJQSl^p4QN6QEQ{BSl+GL$S7Gkm`f{%eF7LE#An2{E$ORLn}rJxvP;AedomlLqgcuVPp zo-k@}6xOJ`z`2tydKmI5>{hm0Z+$>`npAecx{b_>V=syVbU2(~wS&xp^5%y0S|z0@ z+c!6uWA$yXF>4XKO}d@^_&h1~QHTQKneWtrZMk|l6^AaO6ahF=alG+S@msEJpNV0- zH$5}pp3dI`F}V>c4Xc&{XSga2u(^e(1QI()I+TYOqRc^&GR;1Z7iOcJj-Te{{M}c( z{PzzJMh$gV9Gw5q{FXF19Msc+-BCy_uD3Z~7OQy(+j_usi_zi##UaV7Qo>CucsRX; zuqwF~AYUsmb*K}BDQ!L@OR@SlIA_i>YsqYjLIl1GqBBTHGY&}uWe}MyF-Zvg5SAjv zcsNltq|vj|ASBFhb#Io{eO(o~9@bhf0%C``me^mKvV#CC84JkWF3{R{5dq2jKo<^- zI+L<4pqqi8pWlz456Z7fd@2Y%yXzKWHS*%{Lw6WA9&b#sHDFiKoPw3?4dpmstO`S$ z-)i^MeUDcnvr5DpQGyK0fS3|2>8lW--Pq~8iD-yrQ197@()7!#;o>>%4>IWf`xLnD z7_ZX`Hwn_+!knky^?3<5W&L(xv64})#ny6PHT@C}Jh9%v-rEvm4zUzcW4kX!UNlXI zzVmzPLy8o>^t?-q+2p`SL6T?rd*5u82_1qLF9_^GH-d}@PZO975riJyAy=8+}K zbm2F%zOgwkmOT}Y#0oZQC40Ck~>Pz|%N&bNv4wTc9UE{4$nvk1G z`F!VfQL=AWu(XnT6@TVxK-rn6zaZp(ZLMe3%P`6ux10J$Cgz)FDT8bGu)obKCx~QR zrkyK>U@j|1az4~#{$%h9hiDsZ8-NJM}3?;GqPX^19}8c;h4g&E};K zhz+8%KdCX!znh7J0m9rLN4J%xl?Isv8E&$l*NyDAIJE-Mgh70L$v+1txeWPd`z4em zi*HDGhI_^~Kzz=$@r=J%`1DfG_5uIyPcny03f&z_)2hT5Un0jm?Nc#odavBc|LqD7&TTfu{EKi_o z&w+Cso@;m#w-S=*R4Hr6h8gd1cXb`WtJiM^k=v^00US+^_vzI-MYk;!NQu6-n`)Bj zDF@!=WZm8>=6^nZ?9(BS)^epbzc-&;{+QoYce0bOgkBP%#~7pA{NGbLQam=l?(H=T z)vuTA*&upYIn%CH73HsI35gM2Je(_H&aMclVA4l(PSssN&Fhhx)1_WpStPbD4R~~U zfvozR559@Fk!+K8`fyc*gi(l=G8KC6^n>!1%>&O?y9JTB07jSAwARqvC)9?E6%8a3 zR;>YVvrOvb{x=l&X5`#n<1^zbGE5pT?U{cqmmb*o0NTuXC)4DBDv1bBNP|?*8aqzL zo0H~5DKWop^^ON|$SKB*HK8)bJ{h?cQfr2Ho`MrM@m~*iXs%b|lJFR> z@??s>4jR2hS@P+^!(`D25X-GVq|QfGYT#Apw0O^k@^uWO$VhzJ&&0acsBEo}K(r+4 z@FZ*J7Uzn0IoceLg0!Lq+@h_oFPNkViE_30H6GgWMZs=-!eZ~dQz-v_M_bTp;D&Us z;I-jXbyj0_p6v$hvi=i`k!>6+CX zsaB~5{L9$n7G7czIIgC_5`0CgU@iamV731CQ2*KRW9V#1cR}~SJR0BvI+z-K)qK*` z#!-f8oA6UDk)q{p_u@@if|yk=lu5OnKKaFSWS3(>$3krl-RsO+Bt15|OCUXzDxOH? zP2j+_DXlN8zAV|$aKKWrPKwXgNwKPY|Dp1uIY*qq`?5q!D!?@*b+i>n zEKbPLaz>N3kj<>d>p{TFq|R@s{l<7&JtP!_2smZLI;3^y9J3PKQ~sEAqI(u?=La(TJ9yw7V+gJ_lT1EGt=D>_FR4Jh|lG)#n- z@>_~*kmKr@@r(uY|0bg-3}}7~>OyPPaQ7Sk=Jg8|k#fjPYh7#AE1-b7GHnVb16)F^ zdy`$P_m4CmOh-RfIh3|r^Q~rgwXhoh_f_qsT4(und_OR_=gQkCWOBZq8#*DlL3&=Y zExBU7%Ztn6!#lH_M^0496y4(%tD zy>9(2F(|-gT0UnK+Z|WE7^6y5I6B-FCYt7ZVn?Fl(hK*Z_36Sf)L8v&74ExhxUG%X zy*z6Y^e3dDa{c~voj2d#_IiH{ZE1b5vO@5*6l7RZ6LXYFV;;^Y3lTZ{LP;)n>W2|| z`Q05?-3tcLzord=Ti#SRlri0tKQqXm-2zirWuHMU8V}LRv*}?ybe%}7Ten6qB>eX6 zUGDCiAdXA`Zv7`#0a7dqGnPQB1L1Vg3IGXVWw^KkI(vxF+AyT|eOiOe?L=rci8Kxg z*)IdF$Zda7U%kQAa%)n}B6SN0-hK^ts=5C%VN5f+@h=k?eQ-n8qK51ck-|Vh;Qn(} zhtsFfCI9)RJQN8a33*8t7G^T`$9Eel^u>)A|M`HssLZ|5lP!J;t9b+J$I!mw)MO<5 z;uRjw`Bdo%^fa|7eGq(td9EAl9QeWL7P%V%#FXhP0f{koCvyiSh&jjLud5Ax0OfWhG zha|Tmxh@0|<1kl`RKcI4>t|w_wX{S+-w&XFib|V10c1*&8J8|y3e;mlDD+ARdiMy+ zpbrK6kylosHlwSFRG3f~wC2Z1tB{`&DSc*_%hN>dkQC%a0OG(iM@L?-4RHbyO9yZ7 zAXvtDHy+^utVLL!9mx-o$AXDw%tWnE9?7OfdN;{|qs&}bIn+lvQ)6A&-u3Jrf9hDD z#7ab?N9`kB{`te17YOiM%*_QMJKRWtFv|MZr>yt)jZYMYq@hgPc*sM5WAh*d=OTT7 zN&TiB{=O?CJ?2xc_=AP+(pUoQ&VH5z*L5{re6J3Keg988a+HbJsUVJGaKE;aUhms|NUf-6@ zqL7-JH#OQ@2I0lo2X7J4uv=5TRIAF3hm&ER+1u<5!sE%$ zM}_oeQY9cdFqA15U-J7)CPjf#k3_B`{dflDgkVG!%~Gv;2u}a~n)rNBA#OiT&Ad0< z-BqtAEA28ME3pm_kl0Xl71?k+i06&JZ~B3>1d?bp(J(r0vSq=vBJ-Ph%IFZ-P&u+2 zStd&YAZ>AOJ=JKcVRRqQC==q(R0%?NDBE5j;xhc{JL#_%ui;D27^2&CwCVi)N;W!2 z6hJt_;3$M9ef{RmTIdsE^(RHK3>kz=H{Iwz>$8xZpXkdNdRm?+fIg(o>lvFi;Td}T z^#TNwq-+}^@*Zf5hb)mP6CO$dnkg`nU_2e-nY98)EQs7_;Wr@)|6>EpL?HsOjFc*|yTE3% zSeDF;*?>JG4GMp0%SAYE;02#UMWl_K7ENO+6bPYgq0uEuRq{|yz~6_GTj8~c{1}ZA z(iChu2Vt5>u@ej_h?szRV|ok96v{9`a9S%U=t#8u>rd9>O^^QfrVpeK8A4xZq821T zs6vVdc`IPNC^iEp&|5|J??6*y5eku8l#)XiNtE!`fRNC(vjjj^ zDUTpHhE^IpbQ)AnLJzl;9-PT@NhztrXl=%}71g{!1FCdqkOi(cYE4)Uljl?!-Vpa`W#3Zcv#g!uDY{MYkOk0d z8a6{_CP-@CD8Dkp?f>2NBnV+|gN;cwx2Z)?l_?s05nxDrp84RvFdFtL* z1-EGNZR7qbt=Yw#d))q#2Tck@E{)GS|XH}RMF0m}TxRk>0Gx(eoIJ6Sq!F-b;?hlCSh@{%> z1`T{^c4jIEbUX!i@e&Pil{Gc&VD6uu4|~=u6-Af{QikPwi|$Fa=>yq^S)r$;qW=K! z>^;XDi_>OE4xWtG>lNRG<|5G-MLlq4fl!4q&NrHb2<}4QBUYjj%yPpJc$XygJ^*%w z)1T0^JTUqUOJ3ZyJuKoKw3^%CDd=6K!6tOA_5qEgJ}0()Dv;^HCxID-l74Rb3(4a$p_w+)%avu7cmTzg)fP zI&g!|#R9Y?Ua>VP0P~6j#jBPHvGSFDOF`%vCYG@Hc#bX5hx39E$6^C&80FzRTF`!z zZpN{mRVZ_GfM1kJA=I4!yxiB6TaMP~1E9dJ&tX=X$Vd@@Bu}F{`7PQexPvqz^;KB7 zREpM|B%ShT7y=Y~g$n$_E!cfDVJX6~qaJCr6SXiRlOmnN9uv;Uwo|GlYR+M%JRlQwk!NRDDAQqq) zrJIBz9OKT11MudO%|6;OXMh;CqOA@+ng9p4iKco1G!sToUv@epSXQ8^mthOiBP8~z zs39SmRH62_knO^@(f-z3=s2@z^+0+1EBX?3woAf01OjOho(nY~%Im5sYqWXrJC!n! zAF^QKn=oZ%6rlW1l>m!Gczh7>Pjdx_1x`KA5GQF zv^-E+b%sAnkpmcij;h5pAk)$MBpT&>nH5?}N}<;ggh%#4sV`H9x(%idZmHx?4mkIL zA>ZvlT6zTC$N3D?EZ?|wAwR7r>JUR(7pbB?KKAac0!(#fTM5(^X((q?P*c$@0k952 z7N+>vw`{3W9kv2~unUwc(TMEm|JH;aWbd(Gyq19a_1$TyabM?VO6H=PX3-({nkIgm z+fpRWCnw+%8fhGhhIT@tfE;gr#;=Gdtz>Shbr=bgGj45ajQ>d z!uEULq){J%4nqED4L=$+D&sd^omkA1nU*=YU#PjjYP(BQu5jw969|yXD8;GtB%I?H z4W&k!W)uyxA;mfJvpE6OQ=p#AlkED)Gu$FSxA57(h%AQje-(VIp*jMT5aof5>DC=AKQN40c5sUDN7`)2UO4 z`9@8s5fs#1ShQ51QEX?T&$6p~ldX4nL;NwrvLpe>=kzo`_FMv~vmm@Tn%bD0AnR}f z*8`8J2pRWsG$?(C)Ly|ONT_!5)K6v2(Do(mtMY;zBhFJdutYDKR_C)tslcw-Tki0u z4MZNxrTPHskM4iX4v-k|2K9lv;_ENiI7I@!HXQ-nNHDxoz7#6Ro~C8Tj0pB^WrxCrY8z>em~Mg zl++@q5&{6(1Zd*SGEo8rz1N|MN)-K9P&0hTn#qwh<0zR^uPqs5RAOQ}Q$IcNwy<>I z?q0V0`@1Vymn%cfPi~Q^;#|xlm4Mn4vT&XNqAnv=vC2~2%SU8Msc;I3(Tdi^j0EKGZeUXy52+#?Vgjh>|_MHkx_8L>g9cL`9?^>}Su zk(@1CMtLVcw_Cy?BT6hc`^es|s-lS+HIOpGL__KF1Nr40)I`$Fo^S=AXJLc)=rLeN zekXYv#oXi362xRPzt6V%UcI7mGUvLO5*RlKhbExG%1yIh89dROgtdqzci=3-Zr!>i zDTLhfFfjTJjx_G9fCvT{SLdBTq3_QRhdC0Z{)aOwzdDn`0_&l|l<(gVy!dh1h8ofG z1C3HhtHD3+01j~!k~O*`5NuG4qYV+6a}Fk zfEy@q;<7-kcL2x9?@P>>=vB9ya>V1G4KljgWN{THdT}mf$VYu}Zlar%TydueHPVW7 zXTf>fB38`DT~fXR$cH?tev=9#{Yo)f2lZn&$Gq!G_o0lk+!yCs!l%3ex6&lZ+anMn zBhob10K-#@Qna)XUE=#99f?SNv0k522Cbtt-&#Fm_tc%yE%hxmE`?Nxh$J}4(0Pf* zo+Q34Ph1%Yoj}AxU@=JRseto>qzKX=D&#xHd#=iEf>OYJjPn|Swd9(fzrh$HzSJsh z0R52mMi%^G@JVS*1gOkC6B%B1-LIlwuqBRzJkHWji z;NApKZA9KrfzlR-xeL9fz%xm$wiq0_F{t!afNRp5S9{j|juoXfN#X}^ndEW+2DB{m zqInQr;20Sb=^8L9_C7^<#z9Inhu?^D|N8;B2L%X~E%hcy(l}pP!1EG#{(ZWs!Vd@- zZjs6jbEy)}>}%mba`TVTm1#(_zd?7d<1h}aYZI&@R;m?ttubAeGpR8pP_q!K+1Xh1$>lKH#c_POANk?^Fo+i z+#ZvIIv%>P&jzof=_Vq?-YD#Uy8f|mZ*r7NjY`*V<2_YoSpW2&C;i`q<+mO2mfER^ zx}BxshDFN12FudSp16c+K0rGmPLf_^7!X#-vE0qG`>#_Oir`<6UCTW7MH8#*bLN!` zqIKvr_!VnX6CjPc(W(* z&^Iafw?Tt~v2Ma;LosVR=qlLoE}5r;+v5Cx$!Ua?-xI`3-Ifk!9RKjgr(+wTRB;yp zZV9Q5RS74zP@o0oy+B94=)bEM*l4)q1g#;&JmwPrmcLVrup)G% z+-v3(PERI!x}N!X-M>zT57AIoV#%{dYeSRjf8vIhBczG8H=lGuAtaqIGfyy-9FSUL z`x5#0Dje7livJ?jYR3bN{DRl$X{HKaya$Ta8+!FYQQ3e&oo~w}S!M7ZJ>Hjyi@0sa;OB>{Q5HU-r@2^V7OU17RIwOb-8qqm z_BEew0n!zHW2C=dHSFOD9rCvF6Chv+j}1>tQT6DrhDHvZYe+wn+(^WRYcun2Jv9xr zi!-m2_#V-AeYIh3CmMBIS<0>$+<1hWn^+ZV`yZ|Sd_b4Hu<+>rb(1!n#7(xC&3 z*QNPn21M*7bhH1{?|fsy6<*3bol09-CWRiH`MFJ4kS3;rmITlC=PbCHWAu>DpYESJ zV7s^a`OO+U#M|eGThdQtE+ibA7xM++Y$-t1>hGsZnk6rGb$7eY4*1vlYffHj*e-mL z8t%rX+`v)VS}G8_D2r{ypAn0>D(01Ddyb^Y&G-nwe=E%QZQn1=vp=Mr{rGx8FkHwa z(f70#Dgm&m|92k+ochg*0ntd(x`5sZPgKC3{QiE+6p(UsGV2n5^mqOTX9@$<)-Kz0 zvCPb`E0S=4&=;w_rCn*PZ44zn@i-Ju@qYLE;jTACIBQDCI3%({L}r5ycf@0EykCXT z5~Kxf=aSX0y07v$n<-cGLh^rRr>I-auF?mkTJ090i!OvN+JDHE=!q1M8_|z0{qpZ4 zFx@^U|He?ADlMY+f>f&mx@qPF#2X?E;@gObpii$uyRp6$Pbcw+XG-R7D}C|4>M63d zTjzY-ka8-;;Y@n#W_K=*#*Bdk3%4Dx%0@5z4#8PEg8 z(>%9`B)k57a* z++QKxoGT1|19uj&tNq%5OQWJ+joRLV6S2JlS>Zd>h*16FAW44#+^vF(n_2xB(&`pC zPSZSlUK|S0LR=vgX=qf3gaGMr*^{TDFEPNXTmPh?IQdHn2|y(2pWsMAzog~jCX(a; zd?wPCFq=^G63X(^@SVy(L!EZWMiD3@cj8_G?xx$Ps33-Pc6~nH_DmguLDJD5bDxwu zAkPiaoLx0fbOM~fT1$Y6B~b;V^Jc33)FA|^$OST%E6o@$uZE8@$zmv=gN^{ukmew= zSPHmtVW-2q+5f6m4U1pN1*>UVjt)$c;Db>AKkZ%nJJe|#pCKg`MNV66F)AvRbRef% z)(piOvr$B8C7qB%R4cR{s3p^2Dd#EagiP6vY_wwEC|RK`<+vKBQ7MPaes13VAKo9{ z>*~6?a+sO#^Zh>ebKjp+D)mMyf*^(@?+pC{l!iP90#Zx@+l2}hRabS1|GP|^rlyJ* z6s zfNIhEx2KJg1zLF?@ew6Ah2>Z+&^-e&YDUptzA zoSE-k8-$0G6dl-2ygHz87wF>+AgM=oMC>V*nKM%%J}Jo-_B~k#J(3`@8x3*YGNEM8 z0@~{Q5?dw{s3?3Jyo+F1x{dMe3>0wOb->DwZau2QgHE1G9v2E>9u;M{JQ&rSmST!L<;64Fwka+|4gZdbBt zumPq{Z?W4?_+}hA)at+ES5CtZi0B3GP=`&pw6zYigfwibF79uOO~$fK^!<@9w1mqJ z3D%<79=Jv4HR0aB#`n={ z!M(3jf!M_RuciT~4Y)ry*X7@!3OrL>$q*VM>g%uv{`F4Vs_7-j<7JKJ$FuWyKFi|G z4bX}f4MUq<5y6zL7x}6%DCz)A*Nww$gJocL+M>j4;TEI-n9*PZAd5jbHnDxk9idy7 zhNVxi$NdfIBGw&}PymTC{!iCVS_S}roKeZWhxCz3dHhiLjZldf;zux9k1(w8;AsF3 z0MSpK>UzNXf_Qh^to0uOiorSgHG*6wTcJ~?;3D1d@tk5yEOO;tJ^RF`WUXwds;Jn8 zy)sUu@CjPx-$J=Fa0Kh_0Bp$hPLMtWBcuWX zi~t4}_;PQN(>P37$;Q<;Zn&=Nv?(8BDG6=S%`kV?UYbF(%{VG4To2iR&`Y$Z;l|iT zR;WSI1IgnJwKRjIiZp&<;Xn;==V1-Flkw@J=maM{H@sa86tOlGh8&>WSWQ7EaBso4 zcvDkCD1lNhMUV=Ebh)$>5f(;UilVzRKx&*)UlO7f9kXHWS|&m}+JUIZQIaAZ9{>j? zrr3|CeCfBCDtBKe7*bKI5SWdz;Q5>j=f!N%a#$>Bm!mApWkByh zR4bgL`1Jf3dQ(lj(wDqYvntHzVJ$zef=eL?6&1{w1=jV0Jyy)1#od@MHU=g(mO3i7z!`;sWV1<@e1{D;O793aZiYBeoufv3cJRfa#tQPseRpvYVQ1jDd(nwx?mCuZr{?vFO znoErpe9n!?MROEq%7`P6ZQ68L`;^n?mOlbQHmdD)yV>08ai4s4)V{y<|E%7DSpx%= zIW#vjs0?C4CA96iM4;zegbUY~?WTV27%NrG29hWB$H~w94%*3sAa`K9Oi2vveN0*q z+{!0qIcZ*S#`5G(bSlhFWj|GT%uJD0J<#kL@Wy_Qv^-BLQ*Enr^>n!}uh=5ze8^WQ zbyM%6j2lzT~fAFAXlKV zIAx_bA|8Ps6LZY~>WMZd2$@bA)rK6say(W;$3UW^jXJPftD??{-|TAJj~liWs9_Up ziiik%Y&J?*TxWD>pvw!76zUndK~pPhke`QL9rH1^38x|8n>dSAui2%pr|)iG=D_;3 zSyH@%vFdo~dVAJqSfH2g8wvGWt)IIdhb}+HCLmAuHhv}9EUe!l-SVrS_rDJ72*5Gb zBdN{9wfYZwCrRpI-&8HxJF|Bvwwcf9VIo;X zVu5pLAQB|v%1!*^s?+dF*;!6?y*Jf+Vc-IXv;ZjIcy@9wVvNZ8Pj>!O-MG+lCq@3l}vxwJdzI>e+%|Y}SuOO&3ufDcDwLECJv53ic=j6E0WF z-63Mdson-tMmxoiwp&oAqGUd@R4Gx?&&*%a$6fS$^XcjH9@bA_9F?#bt8KntGzcfh`9v2`wf{kX^ z*%K#NCiJ#uh`6}-zN-+xdP3!_X=*3VKHT=e_f<;ujwcGp7$DsRS0B;xWJVptNp+wz z0R8ExO>TW%9u~f#y(KVZMYcH0<}bI|j#+{s^}a)I(CA*kfkuerSX_N4m}9^AekXLt zT8S~&it~86HT%*7DWK3(bn@9F$ppKC>z1Q7X+8Nuu=y`QCb}lD^ek+qe+7eMjpg`mo z(NN&}ZqSZ~KfDnRSl!09)}`XT z3KAQr-Wzsgcp6s*l+P{&Vn<=^b_kWKXwB=L$Kei&wv|rYpR%fB)gMXke=s@78CmZC z?tzY@tD0JGho0Ak=6}kAi4HN=uA7PsDJvg! z){l~;&yh>5CXNu<=PL7WNVlB`Sihjmvwr`QpKE%8-(>6*{P|J*9%VgIiE|+ZH%XXI zNuV*WzL=lHtA>Oq&f>!(g--Z{QbWZ4Y9wre>EYW1K+oW{8%yO-*sFIm&` z>ru+*y62HSv0HbBwv5->NyEu6BATpqPES3dX|zXU$%fQ-ud4JCCS$wtZy`jXYU}S^ z4YQFJrp~;U?KJwe+qc?LXC_=^Zqb<$%T(U`vj6pP@A4lWPtSU`(<+zc`+^?f#z^$% z{pm9i5(6$XGpv z`|_Pw=qG`HEfz}h9lMdvqtt86c`>dkNTw(9h`<2egG|Ba&Q$;{FdWJZP}K1Q;K zPjV9uKcw{Gu_?(2Sy4$3Pl5H}+&E5QC|PZCW@r~>aS#`~u;kG|n1LxaikRl4SWn~w%CjiWNZ|JiVF;os-{*eqoR7(oD|&bsk)*5FSv zOHIK^L$Pq~OfP1>&cO5jf#bpA8v3s-dK%6m)2asZZ$w%GD`6_#1-?)eFyqeH-?VK& m_|E$8m;e9I|NYe%ly)SVWeMt;LHO@;7#tf1>+9>bM*Rcqte%(v literal 0 HcmV?d00001 diff --git a/tests/drawing/matplotlib/test_graph.py b/tests/drawing/matplotlib/test_graph.py new file mode 100644 index 000000000..d188a882c --- /dev/null +++ b/tests/drawing/matplotlib/test_graph.py @@ -0,0 +1,176 @@ +import random +import unittest + + +from igraph import Graph, InternalError, plot, VertexClustering + +# FIXME: find a better way to do this that works for both direct call and module +# import e.g. tox +try: + from .utils import find_image_comparison +except ImportError: + from utils import find_image_comparison + +try: + import matplotlib as mpl + import matplotlib.pyplot as plt +except ImportError: + raise unittest.SkipTest("matplotlib/pyplot not found, skipping tests") + +image_comparison = find_image_comparison() + + +class GraphTestRunner(unittest.TestCase): + @property + def layout_small_ring(self): + coords = [ + [1.015318095035966, 0.03435580194714975], + [0.29010409851547664, 1.0184451153265959], + [-0.8699239050738742, 0.6328259400443561], + [-0.8616466426732888, -0.5895891303732176], + [0.30349699041342515, -0.9594640169691343], + ] + return coords + + @image_comparison(baseline_images=["graph_basic"], remove_text=True) + def test_basic(self): + plt.close("all") + g = Graph.Ring(5) + fig, ax = plt.subplots() + plot(g, target=ax, layout=self.layout_small_ring) + + @image_comparison(baseline_images=["graph_directed"], remove_text=True) + def test_directed(self): + plt.close("all") + g = Graph.Ring(5, directed=True) + fig, ax = plt.subplots() + plot(g, target=ax, layout=self.layout_small_ring) + + @image_comparison(baseline_images=["graph_mark_groups_directed"], remove_text=True) + def test_mark_groups(self): + plt.close("all") + g = Graph.Ring(5, directed=True) + fig, ax = plt.subplots() + plot(g, target=ax, mark_groups=True, layout=self.layout_small_ring) + + @image_comparison( + baseline_images=["graph_mark_groups_squares_directed"], remove_text=True + ) + def test_mark_groups_squares(self): + plt.close("all") + g = Graph.Ring(5, directed=True) + fig, ax = plt.subplots() + plot(g, target=ax, mark_groups=True, vertex_shape="s", + layout=self.layout_small_ring) + + @image_comparison(baseline_images=["graph_edit_children"], remove_text=True) + def test_mark_groups_squares(self): + plt.close("all") + g = Graph.Ring(5) + fig, ax = plt.subplots() + plot(g, target=ax, vertex_shape="o", + layout=self.layout_small_ring) + dot = ax.get_children()[0] + dot.set_facecolor("blue") + dot.radius *= 0.5 + + +class ClusteringTestRunner(unittest.TestCase): + @property + def layout_small_ring(self): + coords = [ + [1.015318095035966, 0.03435580194714975], + [0.29010409851547664, 1.0184451153265959], + [-0.8699239050738742, 0.6328259400443561], + [-0.8616466426732888, -0.5895891303732176], + [0.30349699041342515, -0.9594640169691343], + ] + return coords + + @property + def layout_large_ring(self): + coords = [ + (2.5, 0.0), + (2.4802867532861947, 0.31333308391076065), + (2.4214579028215777, 0.621724717912137), + (2.324441214720628, 0.9203113817116949), + (2.190766700109659, 1.2043841852542883), + (2.0225424859373686, 1.469463130731183), + (1.822421568553529, 1.7113677648217218), + (1.5935599743717241, 1.926283106939473), + (1.3395669874474914, 2.110819813755038), + (1.0644482289126818, 2.262067631165049), + (0.7725424859373686, 2.3776412907378837), + (0.4684532864643113, 2.4557181268217216), + (0.15697629882328326, 2.495066821070679), + (-0.1569762988232835, 2.495066821070679), + (-0.46845328646431206, 2.4557181268217216), + (-0.7725424859373689, 2.3776412907378837), + (-1.0644482289126818, 2.2620676311650487), + (-1.3395669874474923, 2.1108198137550374), + (-1.5935599743717244, 1.926283106939473), + (-1.8224215685535292, 1.7113677648217211), + (-2.022542485937368, 1.4694631307311832), + (-2.190766700109659, 1.204384185254288), + (-2.3244412147206286, 0.9203113817116944), + (-2.4214579028215777, 0.621724717912137), + (-2.4802867532861947, 0.3133330839107602), + (-2.5, -8.040613248383183e-16), + (-2.4802867532861947, -0.3133330839107607), + (-2.4214579028215777, -0.6217247179121376), + (-2.324441214720628, -0.9203113817116958), + (-2.1907667001096587, -1.2043841852542885), + (-2.022542485937368, -1.4694631307311834), + (-1.822421568553529, -1.7113677648217218), + (-1.5935599743717237, -1.9262831069394735), + (-1.339566987447491, -2.1108198137550382), + (-1.0644482289126804, -2.2620676311650496), + (-0.7725424859373689, -2.3776412907378837), + (-0.46845328646431156, -2.4557181268217216), + (-0.156976298823283, -2.495066821070679), + (0.1569762988232843, -2.495066821070679), + (0.46845328646431283, -2.4557181268217216), + (0.7725424859373681, -2.377641290737884), + (1.0644482289126815, -2.262067631165049), + (1.3395669874474918, -2.1108198137550374), + (1.593559974371725, -1.9262831069394726), + (1.8224215685535297, -1.7113677648217207), + (2.0225424859373695, -1.4694631307311814), + (2.190766700109659, -1.2043841852542883), + (2.3244412147206286, -0.9203113817116947), + (2.421457902821578, -0.6217247179121362), + (2.4802867532861947, -0.3133330839107595), + ] + return coords + + @image_comparison(baseline_images=["clustering_directed"], remove_text=True) + def test_clustering_directed_small(self): + plt.close("all") + g = Graph.Ring(5, directed=True) + clu = VertexClustering(g, [0] * 5) + fig, ax = plt.subplots() + plot(clu, target=ax, mark_groups=True, + layout=self.layout_small_ring) + + @image_comparison(baseline_images=["clustering_directed_large"], remove_text=True) + def test_clustering_directed_large(self): + plt.close("all") + g = Graph.Ring(50, directed=True) + clu = VertexClustering(g, [0] * 3 + [1] * 17 + [2] * 30) + fig, ax = plt.subplots() + plot(clu, layout=self.layout_large_ring, target=ax, mark_groups=True) + + +def suite(): + graph = unittest.makeSuite(GraphTestRunner) + clustering = unittest.makeSuite(ClusteringTestRunner) + return unittest.TestSuite([graph, clustering]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/drawing/matplotlib/utils.py b/tests/drawing/matplotlib/utils.py new file mode 100644 index 000000000..c9872a2fd --- /dev/null +++ b/tests/drawing/matplotlib/utils.py @@ -0,0 +1,122 @@ +# Functions adapted from matplotlib.testing. Credit for the original functions +# goes to the amazing folks over at matplotlib. +from pathlib import Path +import sys +import inspect +import functools + +try: + import matplotlib + from matplotlib.testing.decorators import _ImageComparisonBase + from matplotlib.testing.compare import comparable_formats + import matplotlib.pyplot as plt +except ImportError: + matplotlib = None + +__all__ = ("find_image_comparison",) + + +def find_image_comparison(): + def dummy_comparison(*args, **kwargs): + return lambda *args, **kwargs: None + + if matplotlib is None: + return dummy_comparison + return image_comparison + + +# NOTE: Parametrizing this requires pytest (see matplotlib's test suite) +_default_extension = "png" + + +def _unittest_image_comparison( + baseline_images, tol, remove_text, savefig_kwargs, style +): + """ + Decorate function with image comparison for unittest. + This function creates a decorator that wraps a figure-generating function + with image comparison code. + """ + import unittest + + def decorator(func): + old_sig = inspect.signature(func) + + @functools.wraps(func) + @matplotlib.style.context(style) + @functools.wraps(func) + def wrapper(*args, **kwargs): + __tracebackhide__ = True + + img = _ImageComparisonBase( + func, tol=tol, remove_text=remove_text, savefig_kwargs=savefig_kwargs + ) + matplotlib.testing.set_font_settings_for_testing() + + func(*args, **kwargs) + + assert len(plt.get_fignums()) == len( + baseline_images + ), "Test generated {} images but there are {} baseline images".format( + len(plt.get_fignums()), len(baseline_images) + ) + for idx, baseline in enumerate(baseline_images): + img.compare(idx, baseline, _default_extension, _lock=False) + + parameters = list(old_sig.parameters.values()) + new_sig = old_sig.replace(parameters=parameters) + wrapper.__signature__ = new_sig + + return wrapper + + return decorator + + +def image_comparison( + baseline_images, + tol=0.01, + remove_text=False, + savefig_kwarg=None, + # Default of mpl_test_settings fixture and cleanup too. + style=("classic", "_classic_test_patch"), +): + """ + Compare images generated by the test with those specified in + *baseline_images*, which must correspond, else an `ImageComparisonFailure` + exception will be raised. + Parameters + ---------- + baseline_images : list or None + A list of strings specifying the names of the images generated by + calls to `.Figure.savefig`. + If *None*, the test function must use the ``baseline_images`` fixture, + either as a parameter or with `pytest.mark.usefixtures`. This value is + only allowed when using pytest. + tol : float, default: 0 + The RMS threshold above which the test is considered failed. + Due to expected small differences in floating-point calculations, on + 32-bit systems an additional 0.06 is added to this threshold. + remove_text : bool + Remove the title and tick text from the figure before comparison. This + is useful to make the baseline images independent of variations in text + rendering between different versions of FreeType. + This does not remove other, more deliberate, text, such as legends and + annotations. + savefig_kwarg : dict + Optional arguments that are passed to the savefig method. + style : str, dict, or list + The optional style(s) to apply to the image test. The test itself + can also apply additional styles if desired. Defaults to ``["classic", + "_classic_test_patch"]``. + """ + if savefig_kwarg is None: + savefig_kwarg = dict() # default no kwargs to savefig + if sys.maxsize <= 2 ** 32: + tol += 0.06 + return _unittest_image_comparison( + baseline_images=baseline_images, + tol=tol, + remove_text=remove_text, + savefig_kwargs=savefig_kwarg, + style=style, + ) diff --git a/tests/drawing/plotly/__init__.py b/tests/drawing/plotly/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/drawing/plotly/baseline_images/clustering_directed.json b/tests/drawing/plotly/baseline_images/clustering_directed.json new file mode 100644 index 000000000..db9b712ef --- /dev/null +++ b/tests/drawing/plotly/baseline_images/clustering_directed.json @@ -0,0 +1,1056 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.015318095035966 + ], + "y": [ + 0.03435580194714975 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.29010409851547664, + 0.1950972580065446, + 0.1950972580065446, + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959, + 1.0631519409409285, + 0.9737382897122633, + 1.0184451153265959 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.8699239050738742, + -0.9649307455828062, + -0.9649307455828062, + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561, + 0.6775327656586887, + 0.5881191144300235, + 0.6328259400443561 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.8616466426732888, + -0.9566534831822209, + -0.9566534831822209, + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176, + -0.544882304758885, + -0.6342959559875502, + -0.5895891303732176 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.30349699041342515, + 0.2084901499044931, + 0.2084901499044931, + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343, + -0.9147571913548017, + -1.004170842583467, + -0.9594640169691343 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.015318095035966, + 0.9203112545270339, + 0.9203112545270339, + 1.015318095035966 + ], + "y": [ + 0.03435580194714975, + 0.07906262756148238, + -0.010351023667182872, + 0.03435580194714975 + ] + } + ], + "layout": { + "shapes": [ + { + "fillcolor": "rgba(255,0,0,63)", + "line": { + "color": "rgba(255,0,0,255)" + }, + "path": "M 1.1197388996537003,-0.16719602520017673L 0.5279328528097523,-0.9934550030368812C 0.45514612062341064,-1.0950773018577296 0.26321850173201045,-1.1588783220760186 0.14407761502695185,-1.1210570434734595L -0.8246194958406551,-0.8135441121460627C -0.9437603825457136,-0.7757228335435037 -1.0637476544979572,-0.6129044204457357 -1.0645940397451423,-0.4879072859505267L -1.0714757322212005,0.5284051354266215C -1.0723221174683857,0.6534022699218305 -0.9545507108608489,0.8178306034288613 -0.8359329190061271,0.8572618024406833L 0.12851107201115136,1.1778644907130693C 0.2471288638658731,1.2172956897248912 0.4399028680044583,1.1560995754479715 0.5140590802883218,1.0554722621592298L 1.1169999394586567,0.23730319901900315C 1.1911561517425202,0.13667588573026146 1.1925256318400421,-0.06557372637932848 1.1197388996537003,-0.16719602520017673 Z", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.30349699041342515,-0.9594640169691343 L 0.9203112545270339,0.03435580194714975", + "type": "path" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "showticklabels": false, + "visible": false + }, + "yaxis": { + "showticklabels": false, + "visible": false + } + } +} diff --git a/tests/drawing/plotly/baseline_images/clustering_directed_large.json b/tests/drawing/plotly/baseline_images/clustering_directed_large.json new file mode 100644 index 000000000..8f525f7de --- /dev/null +++ b/tests/drawing/plotly/baseline_images/clustering_directed_large.json @@ -0,0 +1,2991 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.5 + ], + "y": [ + 0 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.4802867532861947 + ], + "y": [ + 0.31333308391076065 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.4214579028215777 + ], + "y": [ + 0.621724717912137 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.324441214720628 + ], + "y": [ + 0.9203113817116949 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.190766700109659 + ], + "y": [ + 1.2043841852542883 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.0225424859373686 + ], + "y": [ + 1.469463130731183 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.822421568553529 + ], + "y": [ + 1.7113677648217218 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.5935599743717241 + ], + "y": [ + 1.926283106939473 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.3395669874474914 + ], + "y": [ + 2.110819813755038 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.0644482289126818 + ], + "y": [ + 2.262067631165049 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.7725424859373686 + ], + "y": [ + 2.3776412907378837 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.4684532864643113 + ], + "y": [ + 2.4557181268217216 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.15697629882328326 + ], + "y": [ + 2.495066821070679 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.1569762988232835 + ], + "y": [ + 2.495066821070679 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.46845328646431206 + ], + "y": [ + 2.4557181268217216 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.7725424859373689 + ], + "y": [ + 2.3776412907378837 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.0644482289126818 + ], + "y": [ + 2.2620676311650487 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.3395669874474923 + ], + "y": [ + 2.1108198137550374 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.5935599743717244 + ], + "y": [ + 1.926283106939473 + ] + }, + { + "marker": { + "color": "rgba(0,255,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.8224215685535292 + ], + "y": [ + 1.7113677648217211 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.022542485937368 + ], + "y": [ + 1.4694631307311832 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.190766700109659 + ], + "y": [ + 1.204384185254288 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.3244412147206286 + ], + "y": [ + 0.9203113817116944 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.4214579028215777 + ], + "y": [ + 0.621724717912137 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.4802867532861947 + ], + "y": [ + 0.3133330839107602 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.5 + ], + "y": [ + -8.040613248383183e-16 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.4802867532861947 + ], + "y": [ + -0.3133330839107607 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.4214579028215777 + ], + "y": [ + -0.6217247179121376 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.324441214720628 + ], + "y": [ + -0.9203113817116958 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.1907667001096587 + ], + "y": [ + -1.2043841852542885 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -2.022542485937368 + ], + "y": [ + -1.4694631307311834 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.822421568553529 + ], + "y": [ + -1.7113677648217218 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.5935599743717237 + ], + "y": [ + -1.9262831069394735 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.339566987447491 + ], + "y": [ + -2.1108198137550382 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -1.0644482289126804 + ], + "y": [ + -2.2620676311650496 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.7725424859373689 + ], + "y": [ + -2.3776412907378837 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.46845328646431156 + ], + "y": [ + -2.4557181268217216 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.156976298823283 + ], + "y": [ + -2.495066821070679 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.1569762988232843 + ], + "y": [ + -2.495066821070679 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.46845328646431283 + ], + "y": [ + -2.4557181268217216 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.7725424859373681 + ], + "y": [ + -2.377641290737884 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.0644482289126815 + ], + "y": [ + -2.262067631165049 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.3395669874474918 + ], + "y": [ + -2.1108198137550374 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.593559974371725 + ], + "y": [ + -1.9262831069394726 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.8224215685535297 + ], + "y": [ + -1.7113677648217207 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.0225424859373695 + ], + "y": [ + -1.4694631307311814 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.190766700109659 + ], + "y": [ + -1.2043841852542883 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.3244412147206286 + ], + "y": [ + -0.9203113817116947 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.421457902821578 + ], + "y": [ + -0.6217247179121362 + ] + }, + { + "marker": { + "color": "rgba(0,0,255,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 2.4802867532861947 + ], + "y": [ + -0.3133330839107595 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.4802867532861947, + 2.385279912777263, + 2.385279912777263, + 2.4802867532861947 + ], + "y": [ + 0.31333308391076065, + 0.3580399095250933, + 0.268626258296428, + 0.31333308391076065 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.4214579028215777, + 2.3264510623126458, + 2.3264510623126458, + 2.4214579028215777 + ], + "y": [ + 0.621724717912137, + 0.6664315435264696, + 0.5770178922978044, + 0.621724717912137 + ] + }, + { + "fillcolor": "rgb(204,204,204)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.324441214720628, + 2.229434374211696, + 2.229434374211696, + 2.324441214720628 + ], + "y": [ + 0.9203113817116949, + 0.9650182073260275, + 0.8756045560973623, + 0.9203113817116949 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.190766700109659, + 2.095759859600727, + 2.095759859600727, + 2.190766700109659 + ], + "y": [ + 1.2043841852542883, + 1.2490910108686208, + 1.1596773596399557, + 1.2043841852542883 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.0225424859373686, + 1.9275356454284365, + 1.9275356454284365, + 2.0225424859373686 + ], + "y": [ + 1.469463130731183, + 1.5141699563455155, + 1.4247563051168504, + 1.469463130731183 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.822421568553529, + 1.7274147280445968, + 1.7274147280445968, + 1.822421568553529 + ], + "y": [ + 1.7113677648217218, + 1.7560745904360544, + 1.6666609392073892, + 1.7113677648217218 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.5935599743717241, + 1.498553133862792, + 1.498553133862792, + 1.5935599743717241 + ], + "y": [ + 1.926283106939473, + 1.9709899325538056, + 1.8815762813251404, + 1.926283106939473 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.3395669874474914, + 1.2445601469385592, + 1.2445601469385592, + 1.3395669874474914 + ], + "y": [ + 2.110819813755038, + 2.1555266393693704, + 2.066112988140705, + 2.110819813755038 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.0644482289126818, + 0.9694413884037497, + 0.9694413884037497, + 1.0644482289126818 + ], + "y": [ + 2.262067631165049, + 2.3067744567793818, + 2.2173608055507166, + 2.262067631165049 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.7725424859373686, + 0.6775356454284366, + 0.6775356454284366, + 0.7725424859373686 + ], + "y": [ + 2.3776412907378837, + 2.4223481163522163, + 2.332934465123551, + 2.3776412907378837 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.4684532864643113, + 0.37344644595537924, + 0.37344644595537924, + 0.4684532864643113 + ], + "y": [ + 2.4557181268217216, + 2.5004249524360542, + 2.411011301207389, + 2.4557181268217216 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.15697629882328326, + 0.0619694583143512, + 0.0619694583143512, + 0.15697629882328326 + ], + "y": [ + 2.495066821070679, + 2.5397736466850116, + 2.4503599954563464, + 2.495066821070679 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.1569762988232835, + -0.25198313933221556, + -0.25198313933221556, + -0.1569762988232835 + ], + "y": [ + 2.495066821070679, + 2.5397736466850116, + 2.4503599954563464, + 2.495066821070679 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.46845328646431206, + -0.5634601269732441, + -0.5634601269732441, + -0.46845328646431206 + ], + "y": [ + 2.4557181268217216, + 2.5004249524360542, + 2.411011301207389, + 2.4557181268217216 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.7725424859373689, + -0.8675493264463009, + -0.8675493264463009, + -0.7725424859373689 + ], + "y": [ + 2.3776412907378837, + 2.4223481163522163, + 2.332934465123551, + 2.3776412907378837 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.0644482289126818, + -1.159455069421614, + -1.159455069421614, + -1.0644482289126818 + ], + "y": [ + 2.2620676311650487, + 2.3067744567793813, + 2.217360805550716, + 2.2620676311650487 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.3395669874474923, + -1.4345738279564244, + -1.4345738279564244, + -1.3395669874474923 + ], + "y": [ + 2.1108198137550374, + 2.15552663936937, + 2.0661129881407048, + 2.1108198137550374 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.5935599743717244, + -1.6885668148806565, + -1.6885668148806565, + -1.5935599743717244 + ], + "y": [ + 1.926283106939473, + 1.9709899325538056, + 1.8815762813251404, + 1.926283106939473 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.8224215685535292, + -1.9174284090624614, + -1.9174284090624614, + -1.8224215685535292 + ], + "y": [ + 1.7113677648217211, + 1.7560745904360537, + 1.6666609392073886, + 1.7113677648217211 + ] + }, + { + "fillcolor": "rgb(204,204,204)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.022542485937368, + -2.1175493264463, + -2.1175493264463, + -2.022542485937368 + ], + "y": [ + 1.4694631307311832, + 1.5141699563455158, + 1.4247563051168506, + 1.4694631307311832 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.190766700109659, + -2.285773540618591, + -2.285773540618591, + -2.190766700109659 + ], + "y": [ + 1.204384185254288, + 1.2490910108686206, + 1.1596773596399554, + 1.204384185254288 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.3244412147206286, + -2.4194480552295605, + -2.4194480552295605, + -2.3244412147206286 + ], + "y": [ + 0.9203113817116944, + 0.9650182073260269, + 0.8756045560973618, + 0.9203113817116944 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.4214579028215777, + -2.5164647433305096, + -2.5164647433305096, + -2.4214579028215777 + ], + "y": [ + 0.621724717912137, + 0.6664315435264696, + 0.5770178922978044, + 0.621724717912137 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.4802867532861947, + -2.5752935937951267, + -2.5752935937951267, + -2.4802867532861947 + ], + "y": [ + 0.3133330839107602, + 0.35803990952509285, + 0.26862625829642756, + 0.3133330839107602 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.5, + -2.595006840508932, + -2.595006840508932, + -2.5 + ], + "y": [ + -8.040613248383183e-16, + 0.04470682561433182, + -0.04470682561433343, + -8.040613248383183e-16 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.4802867532861947, + -2.5752935937951267, + -2.5752935937951267, + -2.4802867532861947 + ], + "y": [ + -0.3133330839107607, + -0.26862625829642806, + -0.35803990952509335, + -0.3133330839107607 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.4214579028215777, + -2.5164647433305096, + -2.5164647433305096, + -2.4214579028215777 + ], + "y": [ + -0.6217247179121376, + -0.577017892297805, + -0.6664315435264702, + -0.6217247179121376 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.324441214720628, + -2.41944805522956, + -2.41944805522956, + -2.324441214720628 + ], + "y": [ + -0.9203113817116958, + -0.8756045560973632, + -0.9650182073260284, + -0.9203113817116958 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.1907667001096587, + -2.2857735406185906, + -2.2857735406185906, + -2.1907667001096587 + ], + "y": [ + -1.2043841852542885, + -1.1596773596399559, + -1.249091010868621, + -1.2043841852542885 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -2.022542485937368, + -2.1175493264463, + -2.1175493264463, + -2.022542485937368 + ], + "y": [ + -1.4694631307311834, + -1.4247563051168508, + -1.514169956345516, + -1.4694631307311834 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.822421568553529, + -1.9174284090624611, + -1.9174284090624611, + -1.822421568553529 + ], + "y": [ + -1.7113677648217218, + -1.6666609392073892, + -1.7560745904360544, + -1.7113677648217218 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.5935599743717237, + -1.6885668148806559, + -1.6885668148806559, + -1.5935599743717237 + ], + "y": [ + -1.9262831069394735, + -1.8815762813251409, + -1.970989932553806, + -1.9262831069394735 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.339566987447491, + -1.434573827956423, + -1.434573827956423, + -1.339566987447491 + ], + "y": [ + -2.1108198137550382, + -2.0661129881407057, + -2.155526639369371, + -2.1108198137550382 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -1.0644482289126804, + -1.1594550694216126, + -1.1594550694216126, + -1.0644482289126804 + ], + "y": [ + -2.2620676311650496, + -2.217360805550717, + -2.306774456779382, + -2.2620676311650496 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.7725424859373689, + -0.8675493264463009, + -0.8675493264463009, + -0.7725424859373689 + ], + "y": [ + -2.3776412907378837, + -2.332934465123551, + -2.4223481163522163, + -2.3776412907378837 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.46845328646431156, + -0.5634601269732437, + -0.5634601269732437, + -0.46845328646431156 + ], + "y": [ + -2.4557181268217216, + -2.411011301207389, + -2.5004249524360542, + -2.4557181268217216 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + -0.156976298823283, + -0.25198313933221506, + -0.25198313933221506, + -0.156976298823283 + ], + "y": [ + -2.495066821070679, + -2.4503599954563464, + -2.5397736466850116, + -2.495066821070679 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.1569762988232843, + 0.06196945831435223, + 0.06196945831435223, + 0.1569762988232843 + ], + "y": [ + -2.495066821070679, + -2.4503599954563464, + -2.5397736466850116, + -2.495066821070679 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.46845328646431283, + 0.3734464459553808, + 0.3734464459553808, + 0.46845328646431283 + ], + "y": [ + -2.4557181268217216, + -2.411011301207389, + -2.5004249524360542, + -2.4557181268217216 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 0.7725424859373681, + 0.677535645428436, + 0.677535645428436, + 0.7725424859373681 + ], + "y": [ + -2.377641290737884, + -2.3329344651235515, + -2.4223481163522167, + -2.377641290737884 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.0644482289126815, + 0.9694413884037495, + 0.9694413884037495, + 1.0644482289126815 + ], + "y": [ + -2.262067631165049, + -2.2173608055507166, + -2.3067744567793818, + -2.262067631165049 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.3395669874474918, + 1.2445601469385597, + 1.2445601469385597, + 1.3395669874474918 + ], + "y": [ + -2.1108198137550374, + -2.0661129881407048, + -2.15552663936937, + -2.1108198137550374 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.593559974371725, + 1.4985531338627929, + 1.4985531338627929, + 1.593559974371725 + ], + "y": [ + -1.9262831069394726, + -1.88157628132514, + -1.9709899325538052, + -1.9262831069394726 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 1.8224215685535297, + 1.7274147280445975, + 1.7274147280445975, + 1.8224215685535297 + ], + "y": [ + -1.7113677648217207, + -1.6666609392073881, + -1.7560745904360533, + -1.7113677648217207 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.0225424859373695, + 1.9275356454284374, + 1.9275356454284374, + 2.0225424859373695 + ], + "y": [ + -1.4694631307311814, + -1.4247563051168488, + -1.514169956345514, + -1.4694631307311814 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.190766700109659, + 2.095759859600727, + 2.095759859600727, + 2.190766700109659 + ], + "y": [ + -1.2043841852542883, + -1.1596773596399557, + -1.2490910108686208, + -1.2043841852542883 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.3244412147206286, + 2.2294343742116967, + 2.2294343742116967, + 2.3244412147206286 + ], + "y": [ + -0.9203113817116947, + -0.8756045560973621, + -0.9650182073260273, + -0.9203113817116947 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.421457902821578, + 2.326451062312646, + 2.326451062312646, + 2.421457902821578 + ], + "y": [ + -0.6217247179121362, + -0.5770178922978036, + -0.6664315435264688, + -0.6217247179121362 + ] + }, + { + "fillcolor": "rgb(51,51,51)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.4802867532861947, + 2.385279912777263, + 2.385279912777263, + 2.4802867532861947 + ], + "y": [ + -0.3133330839107595, + -0.26862625829642683, + -0.3580399095250921, + -0.3133330839107595 + ] + }, + { + "fillcolor": "rgb(204,204,204)", + "mode": "lines", + "type": "scatter", + "x": [ + 2.5, + 2.404993159491068, + 2.404993159491068, + 2.5 + ], + "y": [ + 0, + 0.044706825614332625, + -0.044706825614332625, + 0 + ] + } + ], + "layout": { + "shapes": [ + { + "fillcolor": "rgba(0,0,255,63)", + "line": { + "color": "rgba(0,0,255,255)" + }, + "path": "M 0.08467188267459153,-2.7297732924988862L 0.08155152177765243,-2.7299214387141904C -0.009367696129893549,-2.7342380349273507 -0.1909500767190293,-2.7304844316164045 -0.28161323940061905,-2.722414232092298L -0.2847409075439839,-2.722135828987712C -0.3769679042972561,-2.7139264279113124 -0.5593295859951877,-2.684528544914581 -0.6494642709398472,-2.6633400629942487L -0.6518360308590739,-2.6627825196667407C -0.7431565957633468,-2.641315266082655 -0.921699356285265,-2.5853143022647096 -1.0089215519029104,-2.55078059203085L -1.0106573132672392,-2.550093355490123C -1.0978795088848847,-2.5155596452562636 -1.266366877669369,-2.4341616609851098 -1.347632050836208,-2.3872973869478153L -1.3497426540803754,-2.386080237153332C -1.4299525256251309,-2.339824538013279 -1.5830127061525752,-2.236419510991894 -1.6558630151352642,-2.1792701831105616L -1.6583335671282884,-2.1773320940637357C -1.729948600114465,-2.1211518107058165 -1.8648859771397523,-1.999586376639347 -1.9282083211788628,-1.9342012259307966L -1.9303815533160236,-1.9319571979661154C -1.9926172812865535,-1.8676940612399058 -2.1081776870619855,-1.731601158295806 -2.161502364866888,-1.659771392077916L -2.1631196779521384,-1.6575928281259733C -2.215635699214415,-1.5868523438840547 -2.311295509855163,-1.4392954418783066 -2.3544392992336345,-1.3624790241144775L -2.3555105624439965,-1.3605716672622477C -2.398118720217287,-1.2847089279245334 -2.473583896959723,-1.1282792807827042 -2.506440915928869,-1.047712372978589L -2.5070810151735397,-1.0461428204613852C -2.5396179845203504,-0.9663606889158721 -2.5946343855076077,-0.8034009279178234 -2.6171138171480544,-0.7202232984652875L -2.61744961335299,-0.7189807967529462C -2.6397611468909687,-0.6364244181565811 -2.674109931734575,-0.46920125293896836 -2.686147183040202,-0.3845344663177207L -2.6862833838572926,-0.3835764664201169C -2.698252534754375,-0.2993886797476711 -2.711810564600415,-0.1301813690722769 -2.7133994435493722,-0.045161845069328466L -2.713412967029626,-0.04443821545942635C -2.7149950842384567,0.04021949373857103 -2.7078003322721127,0.20909357538870527 -2.699023463096938,0.29330994784084213L -2.698967550250878,0.2938464463318956C -2.690218637498733,0.3777945695385057 -2.662520663182241,0.5439908777104231 -2.643571601617894,0.6262390626757304L -2.643481710215066,0.6266292352814542C -2.6245775943521323,0.7086823339438996 -2.5768702239744856,0.8698584325875697 -2.548066969459773,0.9489814325687944L -2.5479658817830115,0.94925912203067C -2.519213171106679,1.0282432772809569 -2.452251751203909,1.1820960487271361 -2.414043041977471,1.2569646649230286L -2.413944710679318,1.2571573416286943C -2.3757851671019568,1.331929619471754 -2.2905915065957196,1.4762348833329115 -2.2435573896668433,1.5457678693510093L -2.2435573896668433,1.5457678693510093C -2.196523272737967,1.615300855369107 -2.0714375110137118,1.6539310090799837 -1.993385866218333,1.6230281767727623L 2.564155000068931,-0.18142990343059492C 2.6422066448643093,-0.2123327357378162 2.70694608097404,-0.3261200020467362 2.693633872288393,-0.40900443604843495L 2.693633872288393,-0.40900443604843495C 2.680321663602745,-0.4918888700501337 2.6436414186205517,-0.655401953919702 2.620273382324006,-0.7360306037875716L 2.6202131664742097,-0.7362383714668104C 2.5968150222527657,-0.8169709051742993 2.540308327176887,-0.9749629950037181 2.507199776322452,-1.052222551125648L 2.5070833745499557,-1.0524941773080927C 2.4739166228092726,-1.1298895465212448 2.398361167239949,-1.2800398082461453 2.355972463411309,-1.3527947007578938L 2.355770899895785,-1.353140659246398C 2.313281414309383,-1.4260685310023984 2.219703166854112,-1.5661809938114277 2.168614404985243,-1.6333655848644568L 2.168287905206504,-1.6337949504100493C 2.1170358934482656,-1.7011942242358744 2.0066783780538446,-1.8292232783798599 1.947572874417662,-1.88985305869802L 1.9470676576696004,-1.8903713045222426C 1.887709545659387,-1.951260207752514 1.7619957924271628,-2.0653259186720234 1.695640151205152,-2.1185027263612617L 1.6948850690250796,-2.1191078423565233C 1.628151886713033,-2.172587208043392 1.488640566378044,-2.270974288008281 1.4158624283551022,-2.3158820022863016L 1.414767092505202,-2.3165578787338457C 1.34144128655731,-2.361803531235638 1.189782423469918,-2.442934766460868 1.111449366330418,-2.4788203491843053L 1.1099083214293617,-2.479526325642991C 1.0308047418393333,-2.5157648965957717 0.8687095332988997,-2.578137733423335 0.7857179043484942,-2.6042719992981183L 0.783631311463264,-2.6049290724743495C 0.6995963860702434,-2.631391874937248 0.5288537349802286,-2.673472580731123 0.44214600928323433,-2.6890904840620995L 0.43947570913498357,-2.6895714618440083C 0.351432833363864,-2.705429854065939 0.17403092013366794,-2.725530769393378 0.08467188267459153,-2.7297732924988862 Z", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.5,0.0 L 2.385279912777263,0.31333308391076065", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.4802867532861947,0.31333308391076065 L 2.3264510623126458,0.621724717912137", + "type": "path" + }, + { + "line": { + "color": "rgb(204,204,204)", + "width": 2 + }, + "path": "M 2.4214579028215777,0.621724717912137 L 2.229434374211696,0.9203113817116949", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.324441214720628,0.9203113817116949 L 2.095759859600727,1.2043841852542883", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.190766700109659,1.2043841852542883 L 1.9275356454284365,1.469463130731183", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.0225424859373686,1.469463130731183 L 1.7274147280445968,1.7113677648217218", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.822421568553529,1.7113677648217218 L 1.498553133862792,1.926283106939473", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.5935599743717241,1.926283106939473 L 1.2445601469385592,2.110819813755038", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.3395669874474914,2.110819813755038 L 0.9694413884037497,2.262067631165049", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.0644482289126818,2.262067631165049 L 0.6775356454284366,2.3776412907378837", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.7725424859373686,2.3776412907378837 L 0.37344644595537924,2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.4684532864643113,2.4557181268217216 L 0.0619694583143512,2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.15697629882328326,2.495066821070679 L -0.25198313933221556,2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.1569762988232835,2.495066821070679 L -0.5634601269732441,2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.46845328646431206,2.4557181268217216 L -0.8675493264463009,2.3776412907378837", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.7725424859373689,2.3776412907378837 L -1.159455069421614,2.2620676311650487", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.0644482289126818,2.2620676311650487 L -1.4345738279564244,2.1108198137550374", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.3395669874474923,2.1108198137550374 L -1.6885668148806565,1.926283106939473", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.5935599743717244,1.926283106939473 L -1.9174284090624614,1.7113677648217211", + "type": "path" + }, + { + "line": { + "color": "rgb(204,204,204)", + "width": 2 + }, + "path": "M -1.8224215685535292,1.7113677648217211 L -2.1175493264463,1.4694631307311832", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.022542485937368,1.4694631307311832 L -2.285773540618591,1.204384185254288", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.190766700109659,1.204384185254288 L -2.4194480552295605,0.9203113817116944", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.3244412147206286,0.9203113817116944 L -2.5164647433305096,0.621724717912137", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.4214579028215777,0.621724717912137 L -2.5752935937951267,0.3133330839107602", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.4802867532861947,0.3133330839107602 L -2.595006840508932,-8.049116928532385e-16", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.5,-8.040613248383183e-16 L -2.5752935937951267,-0.3133330839107607", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.4802867532861947,-0.3133330839107607 L -2.5164647433305096,-0.6217247179121376", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.4214579028215777,-0.6217247179121376 L -2.41944805522956,-0.9203113817116958", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.324441214720628,-0.9203113817116958 L -2.2857735406185906,-1.2043841852542885", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.1907667001096587,-1.2043841852542885 L -2.1175493264463,-1.4694631307311834", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -2.022542485937368,-1.4694631307311834 L -1.9174284090624611,-1.7113677648217218", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.822421568553529,-1.7113677648217218 L -1.6885668148806559,-1.9262831069394735", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.5935599743717237,-1.9262831069394735 L -1.434573827956423,-2.1108198137550382", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.339566987447491,-2.1108198137550382 L -1.1594550694216126,-2.2620676311650496", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -1.0644482289126804,-2.2620676311650496 L -0.8675493264463009,-2.3776412907378837", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.7725424859373689,-2.3776412907378837 L -0.5634601269732437,-2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.46845328646431156,-2.4557181268217216 L -0.25198313933221506,-2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M -0.156976298823283,-2.495066821070679 L 0.06196945831435223,-2.495066821070679", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.1569762988232843,-2.495066821070679 L 0.3734464459553808,-2.4557181268217216", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.46845328646431283,-2.4557181268217216 L 0.677535645428436,-2.377641290737884", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 0.7725424859373681,-2.377641290737884 L 0.9694413884037495,-2.262067631165049", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.0644482289126815,-2.262067631165049 L 1.2445601469385597,-2.1108198137550374", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.3395669874474918,-2.1108198137550374 L 1.4985531338627929,-1.9262831069394726", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.593559974371725,-1.9262831069394726 L 1.7274147280445975,-1.7113677648217207", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 1.8224215685535297,-1.7113677648217207 L 1.9275356454284374,-1.4694631307311814", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.0225424859373695,-1.4694631307311814 L 2.095759859600727,-1.2043841852542883", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.190766700109659,-1.2043841852542883 L 2.2294343742116967,-0.9203113817116947", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.3244412147206286,-0.9203113817116947 L 2.326451062312646,-0.6217247179121362", + "type": "path" + }, + { + "line": { + "color": "rgb(51,51,51)", + "width": 2 + }, + "path": "M 2.421457902821578,-0.6217247179121362 L 2.385279912777263,-0.3133330839107595", + "type": "path" + }, + { + "line": { + "color": "rgb(204,204,204)", + "width": 2 + }, + "path": "M 2.4802867532861947,-0.3133330839107595 L 2.404993159491068,0.0", + "type": "path" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "showticklabels": false, + "visible": false + }, + "yaxis": { + "showticklabels": false, + "visible": false + } + } +} diff --git a/tests/drawing/plotly/baseline_images/graph_basic.json b/tests/drawing/plotly/baseline_images/graph_basic.json new file mode 100644 index 000000000..48df85504 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_basic.json @@ -0,0 +1,963 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.015318095035966 + ], + "y": [ + 0.03435580194714975 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.29010409851547664,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.8699239050738742,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.8616466426732888,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.30349699041342515,-0.9594640169691343", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.30349699041342515,-0.9594640169691343", + "type": "path" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "showticklabels": false, + "visible": false + }, + "yaxis": { + "showticklabels": false, + "visible": false + } + } +} diff --git a/tests/drawing/plotly/baseline_images/graph_directed.json b/tests/drawing/plotly/baseline_images/graph_directed.json new file mode 100644 index 000000000..b30d80114 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_directed.json @@ -0,0 +1,1048 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.015318095035966 + ], + "y": [ + 0.03435580194714975 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + 0.29010409851547664, + 0.1950972580065446, + 0.1950972580065446, + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959, + 1.0631519409409285, + 0.9737382897122633, + 1.0184451153265959 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + -0.8699239050738742, + -0.9649307455828062, + -0.9649307455828062, + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561, + 0.6775327656586887, + 0.5881191144300235, + 0.6328259400443561 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + -0.8616466426732888, + -0.9566534831822209, + -0.9566534831822209, + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176, + -0.544882304758885, + -0.6342959559875502, + -0.5895891303732176 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + 0.30349699041342515, + 0.2084901499044931, + 0.2084901499044931, + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343, + -0.9147571913548017, + -1.004170842583467, + -0.9594640169691343 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + 1.015318095035966, + 0.9203112545270339, + 0.9203112545270339, + 1.015318095035966 + ], + "y": [ + 0.03435580194714975, + 0.07906262756148238, + -0.010351023667182872, + 0.03435580194714975 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 0.30349699041342515,-0.9594640169691343 L 0.9203112545270339,0.03435580194714975", + "type": "path" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "showticklabels": false, + "visible": false + }, + "yaxis": { + "showticklabels": false, + "visible": false + } + } +} diff --git a/tests/drawing/plotly/baseline_images/graph_edit_children.json b/tests/drawing/plotly/baseline_images/graph_edit_children.json new file mode 100644 index 000000000..48df85504 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_edit_children.json @@ -0,0 +1,963 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.015318095035966 + ], + "y": [ + 0.03435580194714975 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.29010409851547664,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.8699239050738742,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.8616466426732888,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.30349699041342515,-0.9594640169691343", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.30349699041342515,-0.9594640169691343", + "type": "path" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "showticklabels": false, + "visible": false + }, + "yaxis": { + "showticklabels": false, + "visible": false + } + } +} diff --git a/tests/drawing/plotly/baseline_images/graph_mark_groups_directed.json b/tests/drawing/plotly/baseline_images/graph_mark_groups_directed.json new file mode 100644 index 000000000..b30d80114 --- /dev/null +++ b/tests/drawing/plotly/baseline_images/graph_mark_groups_directed.json @@ -0,0 +1,1048 @@ +{ + "data": [ + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 1.015318095035966 + ], + "y": [ + 0.03435580194714975 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176 + ] + }, + { + "marker": { + "color": "rgba(255,0,0,255)", + "line": { + "color": "rgba(0,0,0,255)" + }, + "size": 0.2, + "symbol": "circle" + }, + "mode": "markers", + "type": "scatter", + "x": [ + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + 0.29010409851547664, + 0.1950972580065446, + 0.1950972580065446, + 0.29010409851547664 + ], + "y": [ + 1.0184451153265959, + 1.0631519409409285, + 0.9737382897122633, + 1.0184451153265959 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + -0.8699239050738742, + -0.9649307455828062, + -0.9649307455828062, + -0.8699239050738742 + ], + "y": [ + 0.6328259400443561, + 0.6775327656586887, + 0.5881191144300235, + 0.6328259400443561 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + -0.8616466426732888, + -0.9566534831822209, + -0.9566534831822209, + -0.8616466426732888 + ], + "y": [ + -0.5895891303732176, + -0.544882304758885, + -0.6342959559875502, + -0.5895891303732176 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + 0.30349699041342515, + 0.2084901499044931, + 0.2084901499044931, + 0.30349699041342515 + ], + "y": [ + -0.9594640169691343, + -0.9147571913548017, + -1.004170842583467, + -0.9594640169691343 + ] + }, + { + "fillcolor": "#444", + "mode": "lines", + "type": "scatter", + "x": [ + 1.015318095035966, + 0.9203112545270339, + 0.9203112545270339, + 1.015318095035966 + ], + "y": [ + 0.03435580194714975, + 0.07906262756148238, + -0.010351023667182872, + 0.03435580194714975 + ] + } + ], + "layout": { + "shapes": [ + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 1.015318095035966,0.03435580194714975 L 0.1950972580065446,1.0184451153265959", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 0.29010409851547664,1.0184451153265959 L -0.9649307455828062,0.6328259400443561", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8699239050738742,0.6328259400443561 L -0.9566534831822209,-0.5895891303732176", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M -0.8616466426732888,-0.5895891303732176 L 0.2084901499044931,-0.9594640169691344", + "type": "path" + }, + { + "line": { + "color": "#444", + "width": 2 + }, + "path": "M 0.30349699041342515,-0.9594640169691343 L 0.9203112545270339,0.03435580194714975", + "type": "path" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "showticklabels": false, + "visible": false + }, + "yaxis": { + "showticklabels": false, + "visible": false + } + } +} diff --git a/tests/drawing/plotly/test_graph.py b/tests/drawing/plotly/test_graph.py new file mode 100644 index 000000000..288ebeb7f --- /dev/null +++ b/tests/drawing/plotly/test_graph.py @@ -0,0 +1,185 @@ +import random +import unittest + + +from igraph import Graph, InternalError, plot, VertexClustering + +# FIXME: find a better way to do this that works for both direct call and module +# import e.g. tox +try: + from .utils import find_image_comparison +except ImportError: + from utils import find_image_comparison + +try: + import plotly +except ImportError: + raise unittest.SkipTest("plotly not found, skipping tests") + +# FIXME: trying to debug this specific import on CI +try: + from plotly import graph_objects as go +except ImportError: + raise ImportError('Cannot import graph_objects, dir(plotly): '+str(dir(plotly))) + +image_comparison = find_image_comparison() + + +class GraphTestRunner(unittest.TestCase): + @property + def layout_small_ring(self): + coords = [ + [1.015318095035966, 0.03435580194714975], + [0.29010409851547664, 1.0184451153265959], + [-0.8699239050738742, 0.6328259400443561], + [-0.8616466426732888, -0.5895891303732176], + [0.30349699041342515, -0.9594640169691343], + ] + return coords + + @image_comparison(baseline_images=["graph_basic"]) + def test_basic(self): + g = Graph.Ring(5) + fig = go.Figure() + plot(g, target=fig, layout=self.layout_small_ring) + return fig + + @image_comparison(baseline_images=["graph_directed"]) + def test_directed(self): + g = Graph.Ring(5, directed=True) + fig = go.Figure() + plot(g, target=fig, layout=self.layout_small_ring) + return fig + + @image_comparison(baseline_images=["graph_mark_groups_directed"]) + def test_mark_groups(self): + g = Graph.Ring(5, directed=True) + fig = go.Figure() + plot(g, target=fig, mark_groups=True, + layout=self.layout_small_ring) + return fig + + @image_comparison( + baseline_images=["graph_mark_groups_squares_directed"] + ) + def test_mark_groups_squares(self): + g = Graph.Ring(5, directed=True) + fig = go.Figure() + plot(g, target=fig, mark_groups=True, vertex_shape="square", + layout=self.layout_small_ring) + return fig + + @image_comparison(baseline_images=["graph_edit_children"]) + def test_mark_groups_squares(self): + g = Graph.Ring(5) + fig = go.Figure() + plot(g, target=fig, vertex_shape="circle", + layout=self.layout_small_ring) + # FIXME + #dot = ax.get_children()[0] + #dot.set_facecolor("blue") + #dot.radius *= 0.5 + return fig + + +class ClusteringTestRunner(unittest.TestCase): + @property + def layout_small_ring(self): + coords = [ + [1.015318095035966, 0.03435580194714975], + [0.29010409851547664, 1.0184451153265959], + [-0.8699239050738742, 0.6328259400443561], + [-0.8616466426732888, -0.5895891303732176], + [0.30349699041342515, -0.9594640169691343], + ] + return coords + + @property + def layout_large_ring(self): + coords = [ + (2.5, 0.0), + (2.4802867532861947, 0.31333308391076065), + (2.4214579028215777, 0.621724717912137), + (2.324441214720628, 0.9203113817116949), + (2.190766700109659, 1.2043841852542883), + (2.0225424859373686, 1.469463130731183), + (1.822421568553529, 1.7113677648217218), + (1.5935599743717241, 1.926283106939473), + (1.3395669874474914, 2.110819813755038), + (1.0644482289126818, 2.262067631165049), + (0.7725424859373686, 2.3776412907378837), + (0.4684532864643113, 2.4557181268217216), + (0.15697629882328326, 2.495066821070679), + (-0.1569762988232835, 2.495066821070679), + (-0.46845328646431206, 2.4557181268217216), + (-0.7725424859373689, 2.3776412907378837), + (-1.0644482289126818, 2.2620676311650487), + (-1.3395669874474923, 2.1108198137550374), + (-1.5935599743717244, 1.926283106939473), + (-1.8224215685535292, 1.7113677648217211), + (-2.022542485937368, 1.4694631307311832), + (-2.190766700109659, 1.204384185254288), + (-2.3244412147206286, 0.9203113817116944), + (-2.4214579028215777, 0.621724717912137), + (-2.4802867532861947, 0.3133330839107602), + (-2.5, -8.040613248383183e-16), + (-2.4802867532861947, -0.3133330839107607), + (-2.4214579028215777, -0.6217247179121376), + (-2.324441214720628, -0.9203113817116958), + (-2.1907667001096587, -1.2043841852542885), + (-2.022542485937368, -1.4694631307311834), + (-1.822421568553529, -1.7113677648217218), + (-1.5935599743717237, -1.9262831069394735), + (-1.339566987447491, -2.1108198137550382), + (-1.0644482289126804, -2.2620676311650496), + (-0.7725424859373689, -2.3776412907378837), + (-0.46845328646431156, -2.4557181268217216), + (-0.156976298823283, -2.495066821070679), + (0.1569762988232843, -2.495066821070679), + (0.46845328646431283, -2.4557181268217216), + (0.7725424859373681, -2.377641290737884), + (1.0644482289126815, -2.262067631165049), + (1.3395669874474918, -2.1108198137550374), + (1.593559974371725, -1.9262831069394726), + (1.8224215685535297, -1.7113677648217207), + (2.0225424859373695, -1.4694631307311814), + (2.190766700109659, -1.2043841852542883), + (2.3244412147206286, -0.9203113817116947), + (2.421457902821578, -0.6217247179121362), + (2.4802867532861947, -0.3133330839107595), + ] + return coords + + @image_comparison(baseline_images=["clustering_directed"]) + def test_clustering_directed_small(self): + g = Graph.Ring(5, directed=True) + clu = VertexClustering(g, [0] * 5) + fig = go.Figure() + plot(clu, target=fig, mark_groups=True, layout=self.layout_small_ring) + return fig + + @image_comparison(baseline_images=["clustering_directed_large"]) + def test_clustering_directed_large(self): + g = Graph.Ring(50, directed=True) + clu = VertexClustering(g, [0] * 3 + [1] * 17 + [2] * 30) + fig = go.Figure() + plot(clu, layout=self.layout_large_ring, target=fig, mark_groups=True) + return fig + + +def suite(): + graph = unittest.makeSuite(GraphTestRunner) + clustering = unittest.makeSuite(ClusteringTestRunner) + return unittest.TestSuite([ + graph, + clustering, + ]) + + +def test(): + runner = unittest.TextTestRunner() + runner.run(suite()) + + +if __name__ == "__main__": + test() diff --git a/tests/drawing/plotly/utils.py b/tests/drawing/plotly/utils.py new file mode 100644 index 000000000..eaf54dca9 --- /dev/null +++ b/tests/drawing/plotly/utils.py @@ -0,0 +1,274 @@ +# Functions adapted from matplotlib.testing. Credit for the original functions +# goes to the amazing folks over at matplotlib. +from pathlib import Path +import os +import sys +import inspect +import functools + +try: + import plotly +except ImportError: + plotly = None + +__all__ = ("find_image_comparison",) + + +def find_image_comparison(): + def dummy_comparison(*args, **kwargs): + return lambda *args, **kwargs: None + + if plotly is None: + return dummy_comparison + return image_comparison + + +def _load_baseline_image(filename, fmt): + import json + + if fmt == "json": + with open(filename, "rt") as handle: + image = json.load(handle) + return image + + raise NotImplementedError(f"Image format {fmt} not implemented yet") + + +def _load_baseline_images(filenames, fmt="json"): + baseline_folder = Path(__file__).parent / "baseline_images" + + images = [] + for fn in filenames: + fn_abs = baseline_folder / f"{fn}.{fmt}" + image = _load_baseline_image(fn_abs, fmt) + images.append(image) + return images + + +def _store_result_image_json(fig, result_fn): + import os + import json + + os.makedirs(result_fn.parent, exist_ok=True) + + # This produces a Python dict that's JSON compatible. We print it to a + # file in a way that is easy to diff and lists properties in a predictable + # order + fig_json = fig.to_dict() + with open(result_fn, "wt") as handle: + json.dump(fig_json, handle, indent=2, sort_keys=True) + + +def _store_result_image(image, filename, fmt="json"): + result_folder = Path("result_images") / 'plotly' + result_fn = result_folder / (filename + f".{fmt}") + + if fmt == "json": + return _store_result_image_json(image, result_fn) + + raise NotImplementedError(f"Image format {fmt} not implemented yet") + + +def _compare_image_json(baseline, fig, tol=0.001): + '''This function compares the two JSON dictionaries within some tolerance''' + def is_coords(path_element): + if ',' not in path_element: + return False + coords = path_element.split(',') + for coord in coords: + try: + float(coord) + except ValueError: + return False + return True + + def same_coords(path_elem1, path_elem2, tol): + coords1 = [float(x) for x in path_elem1.split(',')] + coords2 = [float(x) for x in path_elem2.split(',')] + for coord1, coord2 in zip(coords1, coords2): + if abs(coord1 - coord2) > tol: + return False + return True + + # Fig has two keys, 'data' and 'layout' + figd = fig.to_dict() + # 'data' has a list of dots and lines. The order is required to match + if len(baseline['data']) != len(figd['data']): + return False + for stroke1, stroke2 in zip(baseline['data'], figd['data']): + # Some properties are strings, no tolerance + for prop in ['fillcolor', 'mode', 'type']: + if (prop in stroke1) != (prop in stroke2): + return False + if prop not in stroke1: + continue + if stroke1[prop] != stroke2[prop]: + return False + + # Other properties are numeric, recast as float and use tolerance + for prop in ['x', 'y']: + if (prop in stroke1) != (prop in stroke2): + return False + if prop not in stroke1: + continue + if len(stroke1[prop]) != len(stroke2[prop]): + return False + for prop_elem1, prop_elem2 in zip(stroke1[prop], stroke2[prop]): + if abs(float(prop_elem1) - float(prop_elem2)) > tol: + return False + + # 'layout' has a dict of various things, some of which should be identical + if sorted(baseline['layout'].keys()) != sorted(figd['layout'].keys()): + return False + if baseline['layout']['xaxis'] != baseline['layout']['xaxis']: + return False + if baseline['layout']['yaxis'] != baseline['layout']['yaxis']: + return False + # 'shapes' is a list of shape, should be the same up to tolerance + if len(baseline['layout']['shapes']) != len(figd['layout']['shapes']): + return False + for shape1, shape2 in zip(baseline['layout']['shapes'], figd['layout']['shapes']): + if sorted(shape1.keys()) != sorted(shape2.keys()): + return False + if shape1['type'] != shape2['type']: + return False + if 'line' in shape1: + if shape1['line']['color'] != shape2['line']['color']: + return False + if ('width' in shape1['line']) != ('width' in shape2['line']): + return False + if 'width' in shape1['line']: + w1 = float(shape1['line']['width']) + w2 = float(shape2['line']['width']) + if abs(w1 - w2) > tol: + return False + + if 'path' in shape1: + # SVG path + path1, path2 = shape1['path'].split(), shape2['path'].split() + if len(path1) != len(path2): + return False + for path_elem1, path_elem2 in zip(path1, path2): + is_coords1 = is_coords(path_elem1) + is_coords2 = is_coords(path_elem2) + if is_coords1 != is_coords2: + return False + if is_coords1: + if not same_coords(path_elem1, path_elem2, tol): + return False + + # 'layout': skipping that for now, seems mostly plotly internals + + return True + + +def compare_image(baseline, fig, tol=0, fmt="json"): + if fmt == "json": + return _compare_image_json(baseline, fig) + + raise NotImplementedError(f"Image format {fmt} not implemented yet") + + +def _unittest_image_comparison( + baseline_images, + tol, + remove_text, +): + """ + Decorate function with image comparison for unittest. + This function creates a decorator that wraps a figure-generating function + with image comparison code. + """ + import unittest + + def decorator(func): + old_sig = inspect.signature(func) + + # This saves us to lift name, docstring, etc. + # NOTE: not super sure why we need this additional layer of wrapping + # seems to have to do with stripping arguments from the test function + # probably redundant in this adaptation + @functools.wraps(func) + def wrapper(*args, **kwargs): + + # Three steps: + # 1. run the function and store the results + figs = func(*args, **kwargs) + if isinstance(figs, plotly.graph_objects.Figure): + figs = [figs] + + # Store images (used to bootstrap new tests) + for fig, image_file in zip(figs, baseline_images): + _store_result_image(fig, image_file) + + assert len(baseline_images) == len( + figs + ), "Test generated {} images but there are {} baseline images".format( + len(figs), len(baseline_images) + ) + + # 2. load the control images + baselines = _load_baseline_images(baseline_images) + + # 3. compare them one by one + for i, (baseline, fig) in enumerate(zip(baselines, figs)): + if remove_text: + # TODO + pass + + # FIXME: what does tolerance mean for json? + res = compare_image(baseline, fig, tol) + assert res, f"Image {i} does not match the corresponding baseline" + + return figs + + parameters = list(old_sig.parameters.values()) + new_sig = old_sig.replace(parameters=parameters) + wrapper.__signature__ = new_sig + + return wrapper + + return decorator + + +def image_comparison( + baseline_images, + tol=0, + remove_text=False, +): + """ + Compare images generated by the test with those specified in + *baseline_images*, which must correspond, else an `ImageComparisonFailure` + exception will be raised. + Parameters + ---------- + baseline_images : list or None + A list of strings specifying the names of the images generated by + calls to `.Figure.savefig`. + If *None*, the test function must use the ``baseline_images`` fixture, + either as a parameter or with `pytest.mark.usefixtures`. This value is + only allowed when using pytest. + tol : float, default: 0 + The RMS threshold above which the test is considered failed. + Due to expected small differences in floating-point calculations, on + 32-bit systems an additional 0.06 is added to this threshold. + remove_text : bool + Remove the title and tick text from the figure before comparison. This + is useful to make the baseline images independent of variations in text + rendering between different versions of FreeType. + This does not remove other, more deliberate, text, such as legends and + annotations. + savefig_kwarg : dict + Optional arguments that are passed to the savefig method. + style : str, dict, or list + The optional style(s) to apply to the image test. The test itself + can also apply additional styles if desired. Defaults to ``["classic", + "_classic_test_patch"]``. + """ + if sys.maxsize <= 2 ** 32: + tol += 0.06 + return _unittest_image_comparison( + baseline_images=baseline_images, + tol=tol, + remove_text=remove_text, + ) diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 1f77a33be..e7f0d6786 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -105,8 +105,7 @@ def testUnhashableVertexNames(self): # Check the exception self.assertTrue(isinstance(err, RuntimeError)) - if sys.version_info >= (3, 4): - self.assertTrue(repr(value) in str(err)) + self.assertTrue(repr(value) in str(err)) def testVertexNameIndexingBug196(self): g = Graph() diff --git a/tox.ini b/tox.ini index 099458469..1ec3997ed 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,10 @@ deps = numpy; platform_python_implementation != "PyPy" networkx; platform_python_implementation != "PyPy" pandas; platform_python_implementation != "PyPy" -extras = plotting + matplotlib; platform_python_implementation != "PyPy" + pytest; platform_python_implementation != "PyPy" +extras = + test passenv = PATH setenv = TESTING_IN_TOX=1 From df440be8f664dcceb4bd45df75cf9fbd85a4bcab Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 14 Sep 2021 16:24:49 +0200 Subject: [PATCH 0419/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc13f2d05..81ff00f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ [@fwitter](https://github.com/user/fwitter). See PR [#418](https://github.com/igraph/python-igraph/pull/418) for more details. +- Changed default value of the `use_vids=...` argument of `Graph.DataFrame()` + to `True`, thanks to [@fwitter](https://github.com/user/fwitter). + ### Removed - Removed deprecated `UbiGraphDrawer`. From cdf08b03b1dd7d5b78bf1bc276eb61cd34498501 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 17 Sep 2021 12:18:55 +0200 Subject: [PATCH 0420/1681] chore: bumped vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 6f154c7ac..9b636bc9f 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 6f154c7ac0946f7a0baf1ce403c520cacf6035d7 +Subproject commit 9b636bc9fe56ce2162791a081c487e2459c901f6 From e1a31bcbd78cca34777db563052cc0603fd37694 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 20 Sep 2021 12:59:14 +0200 Subject: [PATCH 0421/1681] doc: removed reference to Nexus from tutorial, fixes #438 --- doc/source/assets/zachary.zip | Bin 0 -> 766 bytes doc/source/tutorial.rst | 18 ++++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 doc/source/assets/zachary.zip diff --git a/doc/source/assets/zachary.zip b/doc/source/assets/zachary.zip new file mode 100644 index 0000000000000000000000000000000000000000..d6a99901fad4ba6563510025b0b3fde3c4f464f0 GIT binary patch literal 766 zcmWIWW@Zs#U|`^2NJ%vbK5_0@lNS>MLn2U!pFxJ9Dls`Dv8Ylny(qCDBR3~Bgp+}J zcdAFCUz$f^X$3a}BgR{w?wo9-^5GaRax3!y%M`8 z3Ux4Ce|KAavK8ynq;KK18_)dNy!rFnGizLn$}8)N>SnE5e_Jhd%h##Nvli*TUgmZ6 zx;}s2ns=LggGzs%E)3~=waou&p+p?tyzkFnmi^M({l}&%_x8F^YBO(HY(M|ycfrfo z=GQ;}+++LN^UOZ~d)xi{=XWozJa>23%l18*XYTA>&;K{2HmAF0##A{?!>j*{_$rv!2YqtYqK+qE{#1?8q})_qXfl%F`jWGCQYE)jZq(=ZndbS68N# zrgvTRPQ9!*X_@X>mE1qKmEZq*Gk4~-vwQNcy?&N(S7zPp#igI~?#is6{hdjSbM}E= zmI9Rt%8as{Z#){@nQVnVOgzxfTBmb$E|~g|+$hieC*r|zCVR0TnT_*-#LB||C*NCz z%-LsK+B#FM-S|mtYTMBnJF;9XlZ$#!GW*CSed3t>-k`GQWP0%lKYlrpMs?Q*fr1_B zX5nwRc+Z4Kzu?qPIm9({!!1PtyT%WkE&d9BSY^AD(t6aAO`aFIeHB}~`^H|^w`abJ zeUOw^;knj#EFr6)FQdSk(QFEHK~CF^PY>&L{Ka0oFR){i)@JiAUbx+2t?df+UMrRa zHgERX9EwtkF3Jm*JG?LysN#FG*!Z-&@`2!q7dbvhC3XL=2CC?GSZulA>E1h&&ZY`1 z@MO=p)XlbO#~Vu*StXN~97lIK%O;tyZ*i^czb16+veetm-fvsfOqdo5wkS^6v(Ngp z*~z!6QQLm<26!_v$uZ+floG(i%D@0jbqq@yK`e|U$_h!MXvs6co0ScuoDm2^f%Ifx IDraB-08kA}FaQ7m literal 0 HcmV?d00001 diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index b9d7cf25e..5c12742b9 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -1010,20 +1010,18 @@ Pickled graph ``pickle`` :meth:`Graph.Read_Pickle` :meth:`Graph.write_p As an exercise, download the graph representation of the well-known `Zachary karate club study `_ -from igraph's own graph repository called `Nexus `_, -unzip it and try to load it into |igraph|. Since it is a GraphML file, you must -must use the GraphML reader method from the table above (make sure you use the -appropriate path to the downloaded file): +from :download:`this file `, unzip it and try to load it into +|igraph|. Since it is a GraphML file, you must use the GraphML reader method from +the table above (make sure you use the appropriate path to the downloaded file): ->>> karate = Graph.Read_GraphML("karate.GraphML") +>>> karate = Graph.Read_GraphML("zachary.graphml") >>> summary(karate) IGRAPH UNW- 34 78 -- Zachary's karate club network -+ attr: Author (g), Citation (g), name (g), Faction (v), id (v), name (v), weight (e) If you want to convert the very same graph into, say, Pajek's format, you can do it with the Pajek writer method from the table above: ->>> karate.write_pajek("karate.net") +>>> karate.write_pajek("zachary.net") .. note:: Most of the formats have their own limitations; for instance, not all of them can store attributes. Your best bet is probably GraphML or GML if you @@ -1042,9 +1040,9 @@ the preferred format is again inferred from the extension. The format detection :func:`load` and :meth:`Graph.save` can be overridden by the ``format`` keyword argument which accepts the short names of the formats from the above table: ->>> karate = load("karate.GraphML") ->>> karate.save("karate.net") ->>> karate.save("karate.my_extension", format="gml") +>>> karate = load("zachary.graphml") +>>> karate.save("zachary.net") +>>> karate.save("zachary.my_extension", format="gml") Where to go next From 845fbc9ddda461160ec7962a7c6ebd66e7668307 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 20 Sep 2021 12:59:43 +0200 Subject: [PATCH 0422/1681] doc: added 'doc' extra to setup.py to document what's needed for building the docs --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 4d2a7ffcf..fc8945562 100644 --- a/setup.py +++ b/setup.py @@ -831,6 +831,10 @@ def use_educated_guess(self) -> None: "pandas>=1.1.0,<1.3.1; platform_python_implementation != 'PyPy'", "scipy>=1.5.0; platform_python_implementation != 'PyPy'", ], + "doc": [ + "Sphinx>=4.2.0", + "sphinxbootstrap4theme>=0.6.0" + ] }, python_requires=">=3.6", headers=headers, From 9bde4c29f714a971f6ee5c58a4567b0debd79aab Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 20 Sep 2021 14:05:52 +0200 Subject: [PATCH 0423/1681] doc: update igraph version in example output of the documentation --- doc/source/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 5c12742b9..854c9195b 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -36,7 +36,7 @@ following: >>> import igraph >>> print(igraph.__version__) -0.6 +0.9.6 Another way to make use of |igraph| is to import all its objects and methods into the main Python namespace (so you do not have to type the namespace-qualification every time). @@ -57,7 +57,7 @@ When you start the script, you will see something like this:: $ igraph No configuration file, using defaults - igraph 0.9.4 running inside Python 3.9.6 (default, Jun 29 2021, 05:25:02) + igraph 0.9.6 running inside Python 3.9.6 (default, Jun 29 2021, 05:25:02) Type "copyright", "credits" or "license" for more information. >>> From 8a518d6ed7585f473c3d52af3d84e483d6ed5161 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 22 Sep 2021 00:13:29 +1000 Subject: [PATCH 0424/1681] Graph.is_chordal (#437) * Implement Graph.is_chordal in its simplest form * feat: added 'alpha' and 'alpham1' arguments to is_chordal() * feat: added Graph.maximum_cardinality_search() * feat: added Graph.chordal_completion() * doc: mention that chordal_completion() can be suboptimal Co-authored-by: Tamas Nepusz --- src/_igraph/graphobject.c | 249 ++++++++++++++++++++++++++++++++++++++ tests/test_structural.py | 114 +++++++++++++++++ 2 files changed, 363 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 429e183cd..26a6405c2 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4134,6 +4134,84 @@ PyObject *igraphmodule_Graph_bridges(igraphmodule_GraphObject *self) { return o; } +PyObject *igraphmodule_Graph_chordal_completion( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + static char *kwlist[] = { "alpha", "alpham1", NULL }; + PyObject *alpha_o = Py_None, *alpham1_o = Py_None, *res_o; + igraph_vector_int_t alpha, alpham1, edges; + igraph_vector_int_t *alpha_ptr = 0, *alpham1_ptr = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &alpha_o, &alpham1_o)) { + return NULL; + } + + if (alpha_o != Py_None) { + if (igraphmodule_PyObject_to_vector_int_t(alpha_o, &alpha)) { + return NULL; + } + + alpha_ptr = α + } + + if (alpham1_o != Py_None) { + if (igraphmodule_PyObject_to_vector_int_t(alpham1_o, &alpham1)) { + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + return NULL; + } + + alpham1_ptr = &alpham1; + } + + if (igraph_vector_int_init(&edges, 0)) { + igraphmodule_handle_igraph_error(); + if (alpham1_ptr) { + igraph_vector_int_destroy(alpham1_ptr); + } + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + return NULL; + } + + if (igraph_is_chordal( + &self->g, + alpha_ptr, /* alpha */ + alpham1_ptr, /* alpham1 */ + 0, /* chordal */ + &edges, /* fill_in */ + NULL /* new_graph */ + )) { + igraph_vector_int_destroy(&edges); + + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_int_destroy(alpham1_ptr); + } + + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_int_destroy(alpham1_ptr); + } + + res_o = igraphmodule_vector_int_t_to_PyList_pairs(&edges); + igraph_vector_int_destroy(&edges); + + return res_o; +} + /** \ingroup python_interface_graph * \brief Calculates the closeness centrality of some vertices in a graph. * \return the closeness centralities as a list (or a single float) @@ -5159,6 +5237,68 @@ PyObject *igraphmodule_Graph_hub_score( return res_o; } +PyObject *igraphmodule_Graph_is_chordal( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + static char *kwlist[] = { "alpha", "alpham1", NULL }; + PyObject *alpha_o = Py_None, *alpham1_o = Py_None; + igraph_vector_int_t alpha, alpham1; + igraph_vector_int_t *alpha_ptr = 0, *alpham1_ptr = 0; + igraph_bool_t res; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &alpha_o, &alpham1_o)) { + return NULL; + } + + if (alpha_o != Py_None) { + if (igraphmodule_PyObject_to_vector_int_t(alpha_o, &alpha)) { + return NULL; + } + + alpha_ptr = α + } + + if (alpham1_o != Py_None) { + if (igraphmodule_PyObject_to_vector_int_t(alpham1_o, &alpham1)) { + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + return NULL; + } + + alpham1_ptr = &alpham1; + } + + if (igraph_is_chordal( + &self->g, + alpha_ptr, /* alpha */ + alpham1_ptr, /* alpham1 */ + &res, + NULL, /* fill_in */ + NULL /* new_graph */ + )) { + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_int_destroy(alpham1_ptr); + } + + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (alpha_ptr) { + igraph_vector_int_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_int_destroy(alpham1_ptr); + } + + return res ? Py_True : Py_False; +} /** \ingroup python_interface_graph * \brief Returns the line graph of the graph @@ -5179,6 +5319,51 @@ PyObject *igraphmodule_Graph_linegraph(igraphmodule_GraphObject * self) { return (PyObject *) result; } +/** + * \ingroup python_interface_graph + * \brief Conducts a maximum cardinality search on the graph. + * \sa igraph_maximum_cardinality_search + */ +PyObject *igraphmodule_Graph_maximum_cardinality_search(igraphmodule_GraphObject *self) { + igraph_vector_int_t alpha, alpham1; + PyObject *alpha_o, *alpham1_o; + + if (igraph_vector_int_init(&alpha, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_int_init(&alpham1, 0)) { + igraph_vector_int_destroy(&alpha); + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_maximum_cardinality_search(&self->g, &alpha, &alpham1)) { + igraph_vector_int_destroy(&alpha); + igraph_vector_int_destroy(&alpham1); + return NULL; + } + + alpha_o = igraphmodule_vector_int_t_to_PyList(&alpha); + igraph_vector_int_destroy(&alpha); + + if (!alpha_o) { + igraph_vector_int_destroy(&alpham1); + return NULL; + } + + alpham1_o = igraphmodule_vector_int_t_to_PyList(&alpham1); + igraph_vector_int_destroy(&alpham1); + + if (!alpham1_o) { + Py_DECREF(alpha_o); + return NULL; + } + + return PyTuple_Pack(2, alpha_o, alpham1_o); +} + /** * \ingroup python_interface_graph * \brief Returns the k-neighborhood of some vertices in the @@ -13284,6 +13469,33 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "components in the graph.\n" }, + /* interface to igraph_is_chordal with alternative arguments */ + {"chordal_completion", (PyCFunction)igraphmodule_Graph_chordal_completion, + METH_VARARGS | METH_KEYWORDS, + "chordal_complation(alpha=None, alpham1=None)\n--\n\n" + "Returns the list of edges needed to be added to the graph to make it chordal.\n\n" + "A graph is chordal if each of its cycles of four or more nodes\n" + "has a chord, i.e. an edge joining two nodes that are not\n" + "adjacent in the cycle. An equivalent definition is that any\n" + "chordless cycles have at most three nodes.\n\n" + "The chordal completion of a graph is the list of edges that needed to be\n" + "added to the graph to make it chordal. It is an empty list if the graph is\n" + "already chordal.\n\n" + "Note that at the moment igraph does not guarantee that the returned\n" + "chordal completion is I{minimal}; there may exist a subset of the returned\n" + "chordal completion that is still a valid chordal completion.\n\n" + "@param alpha: the alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the alpha vector; simply passing C{None} here will make igraph\n" + " calculate the alpha vector on its own.\n" + "@param alpham1: the inverse alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the inverse alpha vector; simply passing C{None} here will make\n" + " igraph calculate the inverse alpha vector on its own.\n" + "@return: the list of edges to add to the graph; each item in the list is a\n" + " source-target pair of vertex indices.\n" + }, + /* interface to igraph_closeness */ {"closeness", (PyCFunction) igraphmodule_Graph_closeness, METH_VARARGS | METH_KEYWORDS, @@ -13766,6 +13978,26 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " returned.\n" }, + /* interface to igraph_is_chordal */ + {"is_chordal", (PyCFunction)igraphmodule_Graph_is_chordal, + METH_VARARGS | METH_KEYWORDS, + "is_chordal(alpha=None, alpham1=None)\n--\n\n" + "Returns whether the graph is chordal or not.\n\n" + "A graph is chordal if each of its cycles of four or more nodes\n" + "has a chord, i.e. an edge joining two nodes that are not\n" + "adjacent in the cycle. An equivalent definition is that any\n" + "chordless cycles have at most three nodes.\n\n" + "@param alpha: the alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the alpha vector; simply passing C{None} here will make igraph\n" + " calculate the alpha vector on its own.\n" + "@param alpham1: the inverse alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the inverse alpha vector; simply passing C{None} here will make\n" + " igraph calculate the inverse alpha vector on its own.\n" + "@return: C{True} if the graph is chordal, C{False} otherwise.\n" + }, + /* interface to igraph_avg_nearest_neighbor_degree */ {"knn", (PyCFunction) igraphmodule_Graph_knn, METH_VARARGS | METH_KEYWORDS, @@ -13825,6 +14057,23 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " them).\n" "@param loops: whether self-loops should be counted.\n"}, + /* interface to maximum_cardinality_search */ + {"maximum_cardinality_search", (PyCFunction) igraphmodule_Graph_maximum_cardinality_search, + METH_NOARGS, + "maximum_cardinality_search()\n--\n\n" + "Conducts a maximum cardinality search on the graph. The function computes\n" + "a rank I{alpha} for each vertex such that visiting vertices in decreasing\n" + "rank order corresponds to always choosing the vertex with the most already\n" + "visited neighbors as the next one to visit.\n\n" + "Maximum cardinality search is useful in deciding the chordality of a graph:\n" + "a graph is chordal if and only if any two neighbors of a vertex that are\n" + "higher in rank than the original vertex are connected to each other.\n\n" + "The result of this function can be passed to L{is_chordal()} to speed up\n" + "the chordality computation if you also need the result of the maximum\n" + "cardinality search for other purposes.\n\n" + "@return: a tuple consisting of the rank vector and its inverse.\n" + }, + /* interface to igraph_neighborhood */ {"neighborhood", (PyCFunction) igraphmodule_Graph_neighborhood, METH_VARARGS | METH_KEYWORDS, diff --git a/tests/test_structural.py b/tests/test_structural.py index edec2f8f4..dccf2921d 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -496,6 +496,27 @@ def testNeighborhoodSize(self): class MiscTests(unittest.TestCase): + def assert_valid_maximum_cardinality_search_result(self, graph, alpha, alpham1): + visited = [] + n = graph.vcount() + not_visited = list(range(n)) + + # Check if alpham1 is a valid visiting order + for vertex in reversed(alpham1): + neis = graph.neighbors(vertex) + visited_neis = sum(1 for v in neis if v in visited) + for other_vertex in not_visited: + neis = graph.neighbors(other_vertex) + other_visited_neis = sum(1 for v in neis if v in visited) + self.assertTrue(other_visited_neis <= visited_neis) + + visited.append(vertex) + not_visited.remove(vertex) + + # Check if alpha is the inverse of alpham1 + for index, vertex in enumerate(alpham1): + self.assertEqual(alpha[vertex], index) + def testBridges(self): g = Graph(5, [(0, 1), (1, 2), (2, 0), (0, 3), (3, 4)]) self.assertEqual(g.bridges(), [3, 4]) @@ -504,6 +525,45 @@ def testBridges(self): g = Graph(3, [(0, 1), (1, 2), (2, 3)]) self.assertEqual(g.bridges(), [0, 1, 2]) + def testChordalCompletion(self): + g = Graph() + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Full(3) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Full(5) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Ring(4) + cc = g.chordal_completion() + self.assertEqual(len(cc), 1) + g += cc + self.assertTrue(g.is_chordal()) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Ring(5) + cc = g.chordal_completion() + self.assertEqual(len(cc), 2) + g += cc + self.assertListEqual([], g.chordal_completion()) + + def testChordalCompletionWithHints(self): + g = Graph.Ring(4) + alpha, _ = g.maximum_cardinality_search() + cc = g.chordal_completion(alpha=alpha) + self.assertEqual(len(cc), 1) + g += cc + self.assertTrue(g.is_chordal()) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Ring(5) + _, alpham1 = g.maximum_cardinality_search() + cc = g.chordal_completion(alpham1=alpham1) + self.assertEqual(len(cc), 2) + g += cc + self.assertListEqual([], g.chordal_completion()) + def testConstraint(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) self.assertTrue(isinstance(g.constraint(), list)) # TODO check more @@ -567,6 +627,43 @@ def testIsTree(self): g.is_tree("all") ) + def testIsChordal(self): + g = Graph() + self.assertTrue(g.is_chordal()) + + g = Graph.Full(3) + self.assertTrue(g.is_chordal()) + + g = Graph.Full(5) + self.assertTrue(g.is_chordal()) + + g = Graph.Ring(4) + self.assertFalse(g.is_chordal()) + + g = Graph.Ring(5) + self.assertFalse(g.is_chordal()) + + def testIsChordalWithHint(self): + g = Graph() + alpha, _ = g.maximum_cardinality_search() + self.assertTrue(g.is_chordal(alpha=alpha)) + + g = Graph.Full(3) + alpha, _ = g.maximum_cardinality_search() + self.assertTrue(g.is_chordal(alpha=alpha)) + + g = Graph.Ring(5) + alpha, _ = g.maximum_cardinality_search() + self.assertFalse(g.is_chordal(alpha=alpha)) + + g = Graph.Ring(4) + _, alpham1 = g.maximum_cardinality_search() + self.assertFalse(g.is_chordal(alpham1=alpham1)) + + g = Graph.Full(5) + _, alpham1 = g.maximum_cardinality_search() + self.assertTrue(g.is_chordal(alpham1=alpham1)) + def testLineGraph(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) el = g.linegraph().get_edgelist() @@ -580,6 +677,23 @@ def testLineGraph(self): el.sort() self.assertTrue(el == [(0, 2), (0, 4)]) + def testMaximumCardinalitySearch(self): + g = Graph() + alpha, alpham1 = g.maximum_cardinality_search() + self.assertListEqual([], alpha) + self.assertListEqual([], alpham1) + + g = Graph.Famous("petersen") + alpha, alpham1 = g.maximum_cardinality_search() + + print(repr(alpha), repr(alpham1)) + self.assert_valid_maximum_cardinality_search_result(g, alpha, alpham1) + + g = Graph.GRG(100, 0.2) + alpha, alpham1 = g.maximum_cardinality_search() + + self.assert_valid_maximum_cardinality_search_result(g, alpha, alpham1) + class PathTests(unittest.TestCase): def testShortestPaths(self): From 25e4f2e65a990b10540d374b34dc62b5e4f517d7 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 22 Sep 2021 00:13:29 +1000 Subject: [PATCH 0425/1681] Graph.is_chordal (#437) * Implement Graph.is_chordal in its simplest form * feat: added 'alpha' and 'alpham1' arguments to is_chordal() * feat: added Graph.maximum_cardinality_search() * feat: added Graph.chordal_completion() * doc: mention that chordal_completion() can be suboptimal Co-authored-by: Tamas Nepusz --- src/_igraph/graphobject.c | 249 ++++++++++++++++++++++++++++++++++++++ tests/test_structural.py | 114 +++++++++++++++++ 2 files changed, 363 insertions(+) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 2bb48112b..ca0409bf9 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -4190,6 +4190,84 @@ PyObject *igraphmodule_Graph_bridges(igraphmodule_GraphObject *self) { return o; } +PyObject *igraphmodule_Graph_chordal_completion( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + static char *kwlist[] = { "alpha", "alpham1", NULL }; + PyObject *alpha_o = Py_None, *alpham1_o = Py_None, *res_o; + igraph_vector_t alpha, alpham1, edges; + igraph_vector_t *alpha_ptr = 0, *alpham1_ptr = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &alpha_o, &alpham1_o)) { + return NULL; + } + + if (alpha_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(alpha_o, &alpha, IGRAPHMODULE_TYPE_INT)) { + return NULL; + } + + alpha_ptr = α + } + + if (alpham1_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(alpham1_o, &alpham1, IGRAPHMODULE_TYPE_INT)) { + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + return NULL; + } + + alpham1_ptr = &alpham1; + } + + if (igraph_vector_init(&edges, 0)) { + igraphmodule_handle_igraph_error(); + if (alpham1_ptr) { + igraph_vector_destroy(alpham1_ptr); + } + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + return NULL; + } + + if (igraph_is_chordal( + &self->g, + alpha_ptr, /* alpha */ + alpham1_ptr, /* alpham1 */ + 0, /* chordal */ + &edges, /* fill_in */ + NULL /* new_graph */ + )) { + igraph_vector_destroy(&edges); + + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_destroy(alpham1_ptr); + } + + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_destroy(alpham1_ptr); + } + + res_o = igraphmodule_vector_t_to_PyList_pairs(&edges); + igraph_vector_destroy(&edges); + + return res_o; +} + /** \ingroup python_interface_graph * \brief Calculates the closeness centrality of some vertices in a graph. * \return the closeness centralities as a list (or a single float) @@ -5190,6 +5268,68 @@ PyObject *igraphmodule_Graph_hub_score( return res_o; } +PyObject *igraphmodule_Graph_is_chordal( + igraphmodule_GraphObject *self, PyObject *args, PyObject *kwds +) { + static char *kwlist[] = { "alpha", "alpham1", NULL }; + PyObject *alpha_o = Py_None, *alpham1_o = Py_None; + igraph_vector_t alpha, alpham1; + igraph_vector_t *alpha_ptr = 0, *alpham1_ptr = 0; + igraph_bool_t res; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", kwlist, &alpha_o, &alpham1_o)) { + return NULL; + } + + if (alpha_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(alpha_o, &alpha, IGRAPHMODULE_TYPE_INT)) { + return NULL; + } + + alpha_ptr = α + } + + if (alpham1_o != Py_None) { + if (igraphmodule_PyObject_to_vector_t(alpham1_o, &alpham1, IGRAPHMODULE_TYPE_INT)) { + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + return NULL; + } + + alpham1_ptr = &alpham1; + } + + if (igraph_is_chordal( + &self->g, + alpha_ptr, /* alpha */ + alpham1_ptr, /* alpham1 */ + &res, + NULL, /* fill_in */ + NULL /* new_graph */ + )) { + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_destroy(alpham1_ptr); + } + + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (alpha_ptr) { + igraph_vector_destroy(alpha_ptr); + } + + if (alpham1_ptr) { + igraph_vector_destroy(alpham1_ptr); + } + + return res ? Py_True : Py_False; +} /** \ingroup python_interface_graph * \brief Returns the line graph of the graph @@ -5210,6 +5350,51 @@ PyObject *igraphmodule_Graph_linegraph(igraphmodule_GraphObject * self) { return (PyObject *) result; } +/** + * \ingroup python_interface_graph + * \brief Conducts a maximum cardinality search on the graph. + * \sa igraph_maximum_cardinality_search + */ +PyObject *igraphmodule_Graph_maximum_cardinality_search(igraphmodule_GraphObject *self) { + igraph_vector_t alpha, alpham1; + PyObject *alpha_o, *alpham1_o; + + if (igraph_vector_init(&alpha, 0)) { + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_vector_init(&alpham1, 0)) { + igraph_vector_destroy(&alpha); + igraphmodule_handle_igraph_error(); + return NULL; + } + + if (igraph_maximum_cardinality_search(&self->g, &alpha, &alpham1)) { + igraph_vector_destroy(&alpha); + igraph_vector_destroy(&alpham1); + return NULL; + } + + alpha_o = igraphmodule_vector_t_to_PyList(&alpha, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(&alpha); + + if (!alpha_o) { + igraph_vector_destroy(&alpham1); + return NULL; + } + + alpham1_o = igraphmodule_vector_t_to_PyList(&alpham1, IGRAPHMODULE_TYPE_INT); + igraph_vector_destroy(&alpham1); + + if (!alpham1_o) { + Py_DECREF(alpha_o); + return NULL; + } + + return PyTuple_Pack(2, alpha_o, alpham1_o); +} + /** * \ingroup python_interface_graph * \brief Returns the k-neighborhood of some vertices in the @@ -13174,6 +13359,33 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "components in the graph.\n" }, + /* interface to igraph_is_chordal with alternative arguments */ + {"chordal_completion", (PyCFunction)igraphmodule_Graph_chordal_completion, + METH_VARARGS | METH_KEYWORDS, + "chordal_complation(alpha=None, alpham1=None)\n--\n\n" + "Returns the list of edges needed to be added to the graph to make it chordal.\n\n" + "A graph is chordal if each of its cycles of four or more nodes\n" + "has a chord, i.e. an edge joining two nodes that are not\n" + "adjacent in the cycle. An equivalent definition is that any\n" + "chordless cycles have at most three nodes.\n\n" + "The chordal completion of a graph is the list of edges that needed to be\n" + "added to the graph to make it chordal. It is an empty list if the graph is\n" + "already chordal.\n\n" + "Note that at the moment igraph does not guarantee that the returned\n" + "chordal completion is I{minimal}; there may exist a subset of the returned\n" + "chordal completion that is still a valid chordal completion.\n\n" + "@param alpha: the alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the alpha vector; simply passing C{None} here will make igraph\n" + " calculate the alpha vector on its own.\n" + "@param alpham1: the inverse alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the inverse alpha vector; simply passing C{None} here will make\n" + " igraph calculate the inverse alpha vector on its own.\n" + "@return: the list of edges to add to the graph; each item in the list is a\n" + " source-target pair of vertex indices.\n" + }, + /* interface to igraph_closeness */ {"closeness", (PyCFunction) igraphmodule_Graph_closeness, METH_VARARGS | METH_KEYWORDS, @@ -13656,6 +13868,26 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " returned.\n" }, + /* interface to igraph_is_chordal */ + {"is_chordal", (PyCFunction)igraphmodule_Graph_is_chordal, + METH_VARARGS | METH_KEYWORDS, + "is_chordal(alpha=None, alpham1=None)\n--\n\n" + "Returns whether the graph is chordal or not.\n\n" + "A graph is chordal if each of its cycles of four or more nodes\n" + "has a chord, i.e. an edge joining two nodes that are not\n" + "adjacent in the cycle. An equivalent definition is that any\n" + "chordless cycles have at most three nodes.\n\n" + "@param alpha: the alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the alpha vector; simply passing C{None} here will make igraph\n" + " calculate the alpha vector on its own.\n" + "@param alpham1: the inverse alpha vector from the result of calling\n" + " L{maximum_cardinality_search()} on the graph. Useful only if you already\n" + " have the inverse alpha vector; simply passing C{None} here will make\n" + " igraph calculate the inverse alpha vector on its own.\n" + "@return: C{True} if the graph is chordal, C{False} otherwise.\n" + }, + /* interface to igraph_avg_nearest_neighbor_degree */ {"knn", (PyCFunction) igraphmodule_Graph_knn, METH_VARARGS | METH_KEYWORDS, @@ -13715,6 +13947,23 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " them).\n" "@param loops: whether self-loops should be counted.\n"}, + /* interface to maximum_cardinality_search */ + {"maximum_cardinality_search", (PyCFunction) igraphmodule_Graph_maximum_cardinality_search, + METH_NOARGS, + "maximum_cardinality_search()\n--\n\n" + "Conducts a maximum cardinality search on the graph. The function computes\n" + "a rank I{alpha} for each vertex such that visiting vertices in decreasing\n" + "rank order corresponds to always choosing the vertex with the most already\n" + "visited neighbors as the next one to visit.\n\n" + "Maximum cardinality search is useful in deciding the chordality of a graph:\n" + "a graph is chordal if and only if any two neighbors of a vertex that are\n" + "higher in rank than the original vertex are connected to each other.\n\n" + "The result of this function can be passed to L{is_chordal()} to speed up\n" + "the chordality computation if you also need the result of the maximum\n" + "cardinality search for other purposes.\n\n" + "@return: a tuple consisting of the rank vector and its inverse.\n" + }, + /* interface to igraph_neighborhood */ {"neighborhood", (PyCFunction) igraphmodule_Graph_neighborhood, METH_VARARGS | METH_KEYWORDS, diff --git a/tests/test_structural.py b/tests/test_structural.py index 9ea13d990..eaeef42d6 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -496,6 +496,27 @@ def testNeighborhoodSize(self): class MiscTests(unittest.TestCase): + def assert_valid_maximum_cardinality_search_result(self, graph, alpha, alpham1): + visited = [] + n = graph.vcount() + not_visited = list(range(n)) + + # Check if alpham1 is a valid visiting order + for vertex in reversed(alpham1): + neis = graph.neighbors(vertex) + visited_neis = sum(1 for v in neis if v in visited) + for other_vertex in not_visited: + neis = graph.neighbors(other_vertex) + other_visited_neis = sum(1 for v in neis if v in visited) + self.assertTrue(other_visited_neis <= visited_neis) + + visited.append(vertex) + not_visited.remove(vertex) + + # Check if alpha is the inverse of alpham1 + for index, vertex in enumerate(alpham1): + self.assertEqual(alpha[vertex], index) + def testBridges(self): g = Graph(5, [(0, 1), (1, 2), (2, 0), (0, 3), (3, 4)]) self.assertTrue(g.bridges() == [3, 4]) @@ -504,6 +525,45 @@ def testBridges(self): g = Graph(3, [(0, 1), (1, 2), (2, 3)]) self.assertTrue(g.bridges() == [0, 1, 2]) + def testChordalCompletion(self): + g = Graph() + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Full(3) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Full(5) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Ring(4) + cc = g.chordal_completion() + self.assertEqual(len(cc), 1) + g += cc + self.assertTrue(g.is_chordal()) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Ring(5) + cc = g.chordal_completion() + self.assertEqual(len(cc), 2) + g += cc + self.assertListEqual([], g.chordal_completion()) + + def testChordalCompletionWithHints(self): + g = Graph.Ring(4) + alpha, _ = g.maximum_cardinality_search() + cc = g.chordal_completion(alpha=alpha) + self.assertEqual(len(cc), 1) + g += cc + self.assertTrue(g.is_chordal()) + self.assertListEqual([], g.chordal_completion()) + + g = Graph.Ring(5) + _, alpham1 = g.maximum_cardinality_search() + cc = g.chordal_completion(alpham1=alpham1) + self.assertEqual(len(cc), 2) + g += cc + self.assertListEqual([], g.chordal_completion()) + def testConstraint(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) self.assertTrue(isinstance(g.constraint(), list)) # TODO check more @@ -567,6 +627,43 @@ def testIsTree(self): g.is_tree("all") ) + def testIsChordal(self): + g = Graph() + self.assertTrue(g.is_chordal()) + + g = Graph.Full(3) + self.assertTrue(g.is_chordal()) + + g = Graph.Full(5) + self.assertTrue(g.is_chordal()) + + g = Graph.Ring(4) + self.assertFalse(g.is_chordal()) + + g = Graph.Ring(5) + self.assertFalse(g.is_chordal()) + + def testIsChordalWithHint(self): + g = Graph() + alpha, _ = g.maximum_cardinality_search() + self.assertTrue(g.is_chordal(alpha=alpha)) + + g = Graph.Full(3) + alpha, _ = g.maximum_cardinality_search() + self.assertTrue(g.is_chordal(alpha=alpha)) + + g = Graph.Ring(5) + alpha, _ = g.maximum_cardinality_search() + self.assertFalse(g.is_chordal(alpha=alpha)) + + g = Graph.Ring(4) + _, alpham1 = g.maximum_cardinality_search() + self.assertFalse(g.is_chordal(alpham1=alpham1)) + + g = Graph.Full(5) + _, alpham1 = g.maximum_cardinality_search() + self.assertTrue(g.is_chordal(alpham1=alpham1)) + def testLineGraph(self): g = Graph(4, [(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]) el = g.linegraph().get_edgelist() @@ -580,6 +677,23 @@ def testLineGraph(self): el.sort() self.assertTrue(el == [(0, 2), (0, 4)]) + def testMaximumCardinalitySearch(self): + g = Graph() + alpha, alpham1 = g.maximum_cardinality_search() + self.assertListEqual([], alpha) + self.assertListEqual([], alpham1) + + g = Graph.Famous("petersen") + alpha, alpham1 = g.maximum_cardinality_search() + + print(repr(alpha), repr(alpham1)) + self.assert_valid_maximum_cardinality_search_result(g, alpha, alpham1) + + g = Graph.GRG(100, 0.2) + alpha, alpham1 = g.maximum_cardinality_search() + + self.assert_valid_maximum_cardinality_search_result(g, alpha, alpham1) + class PathTests(unittest.TestCase): def testShortestPaths(self): From 2fded26f050ff55cb72e188490262c8f71384bb6 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 21 Sep 2021 16:25:37 +0200 Subject: [PATCH 0426/1681] chore: updated changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6d9c4755..312aeb0b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## [Unreleased] +### Added + +* Added `Graph.is_chordal()` to test whether a graph is chordal and + `Graph.chordal_completion()` to return a possible (not necessary minimal) + chordal completion of a graph. + +* Added `Graph.maximum_cardinality_search()`, primarily as an aid for + `Graph.is_chordal()`. + ### Changed * Improved performance of `Graph.DataFrame()`, thanks to From 55df7b2409155521d60f4f90d0ba777e555b9857 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 25 Sep 2021 22:44:39 +0200 Subject: [PATCH 0427/1681] ci: build macOS x86_64 and arm64 wheels separately --- .github/workflows/build.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afd7a2df1..9f9fd128c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ env: CIBW_TEST_SKIP: "cp310-*" jobs: - build_wheels: + build_wheel_linux: name: Build wheels on Linux (${{ matrix.wheel_arch }}) runs-on: ubuntu-20.04 strategy: @@ -36,8 +36,8 @@ jobs: with: path: ./wheelhouse/*.whl - build_aarch64_wheels: - name: Build wheels on Linux AArch64 + build_wheel_linux_aarch64: + name: Build wheels on Linux (aarch64) if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-20.04 steps: @@ -62,8 +62,15 @@ jobs: path: ./wheelhouse/*.whl build_wheel_macos: - name: Build wheels on macOS + name: Build wheels on macOS (${{ matrix.wheel_arch }}) runs-on: macos-10.15 + strategy: + matrix: + include: + - cmake_arch: x86_64 + wheel_arch: x86_64 + - cmake_arch: arm64 + wheel_arch: arm64 steps: - uses: actions/checkout@v2 @@ -76,7 +83,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-cache-v1-${{ runner.os }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} + key: C-core-cache-v1-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python @@ -91,8 +98,9 @@ jobs: - name: Build wheels uses: joerick/cibuildwheel@v2.1.1 env: - CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_APPLE_SILICON_PROCESSOR=${{ matrix.cmake_arch }} - uses: actions/upload-artifact@v2 with: From 604ef60e5031652cea88b24ddcd4c2b89e2f1944 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 26 Sep 2021 17:09:49 +0200 Subject: [PATCH 0428/1681] chore: more project URLs in setup.py --- setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.py b/setup.py index 020e8a6bb..aa8e4c816 100644 --- a/setup.py +++ b/setup.py @@ -817,6 +817,12 @@ def use_educated_guess(self) -> None: license="GNU General Public License (GPL)", author="Tamas Nepusz", author_email="ntamas@gmail.com", + project_urls={ + "Bug Tracker": "https://github.com/igraph/python-igraph/issues", + "CI": "https://github.com/igraph/python-igraph/actions", + "Documentation": "https://igraph.org/python/doc", + "Source Code": "https://github.com/igraph/python-igraph", + }, ext_modules=[igraph_extension], package_dir={"igraph": "src/igraph"}, packages=find_packages(where="src"), From f322b9342058d87c3d2595f737bd1f0251c66716 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Sep 2021 17:31:52 +0200 Subject: [PATCH 0429/1681] ci: another attempt at compiling the C core for Apple Silicon --- .github/workflows/build.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f9fd128c..395e66f78 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,7 @@ jobs: - cmake_arch: x86_64 wheel_arch: x86_64 - cmake_arch: arm64 + cmake_extra_args: -DF2C_EXTERNAL_ARITH_HEADER=etc/arith_apple_m1.h wheel_arch: arm64 steps: @@ -83,7 +84,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-cache-v1-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} + key: C-core-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python @@ -100,7 +101,7 @@ jobs: env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" - IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_APPLE_SILICON_PROCESSOR=${{ matrix.cmake_arch }} + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} - uses: actions/upload-artifact@v2 with: From 7b9267f536c244754ca796362f1f957dd53507db Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 29 Sep 2021 01:39:22 +1000 Subject: [PATCH 0430/1681] Construct graph from a few more input data structures (mimicking networkx) (#434) * First pass, poor docs * Forgot classmethod decorator * Some polishing and one more input constructor * Graph constructor from pandas adjacency matrix * Move adjacency constructors to module * Move more constructors to dedicated module * Clean up namespace at the end * Small constructor modules * IO module * fix: remove licenses from igraph.io, it is enough to have it in __init__.py * refactor: prefix names of internal functions in igraph.io with an underscore * refactor: moved _first to igraph.utils * style: blackened igraph.io source * Export to dict of sequences * Recover and refine lost PR on use_vids * Match export graph functions for dict/list/tuple types * Include PR #408 in this one * Docstrings and documentation * Bugfix on dict-dict * Implement most of Vincent's suggestions * Running local tests helps... sigh * style: nitpicking, probably ready to merge Co-authored-by: Tamas Nepusz --- doc/source/generation.rst | 23 +- src/igraph/__init__.py | 1830 +++--------------------------------- src/igraph/io/__init__.py | 0 src/igraph/io/adjacency.py | 146 +++ src/igraph/io/bipartite.py | 152 +++ src/igraph/io/files.py | 487 ++++++++++ src/igraph/io/images.py | 365 +++++++ src/igraph/io/libraries.py | 267 ++++++ src/igraph/io/objects.py | 831 ++++++++++++++++ src/igraph/io/random.py | 17 + tests/test_basic.py | 86 ++ tests/test_foreign.py | 157 +++- tests/test_generators.py | 3 +- 13 files changed, 2651 insertions(+), 1713 deletions(-) create mode 100644 src/igraph/io/__init__.py create mode 100644 src/igraph/io/adjacency.py create mode 100644 src/igraph/io/bipartite.py create mode 100644 src/igraph/io/files.py create mode 100644 src/igraph/io/images.py create mode 100644 src/igraph/io/libraries.py create mode 100644 src/igraph/io/objects.py create mode 100644 src/igraph/io/random.py diff --git a/doc/source/generation.rst b/doc/source/generation.rst index d41da27d4..b83317f6f 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -29,13 +29,30 @@ To specify edge weights (or any other vertex/edge attributes), use dictionaries: >>> edge_attrs={'weight': [0.1, 0.2]}, >>> vertex_attrs={'color': ['b', 'g', 'g', 'y']}) -Variations on this constructor is :meth:`Graph.DictList`, which constructs a graph from a list-of-dictionaries representation, and :meth:`Graph.TupleList`, which constructs a graph from a list-of-tuples representation. - To create a bipartite graph from a list of types and a list of edges, use :meth:`Graph.Bipartite`. +From Python builtin structures (lists, tuples, dicts) ++++++++++++++++++++++++++++++++++++++++++++++++++++++ +|igraph| supports a number of "conversion" methods to import graphs from Python builtin data structures such as dictionaries, lists and tuples: + + - :meth:`Graph.DictList`: from a list of dictionaries + - :meth:`Graph.TupleList`: from a list of tuples + - :meth:`Graph.ListDict`: from a dict of lists + - :meth:`Graph.DictDict`: from a dict of dictionaries + +Equivalent methods are available to export a graph, i.e. to convert a graph into +a representation that uses Python builtin data structures: + + - :meth:`Graph.to_dict_list` + - :meth:`Graph.to_tuple_list` + - :meth:`Graph.to_list_dict` + - :meth:`Graph.to_dict_dict` + +See the `API documentation`_ of each function for details and examples. + From matrices +++++++++++++ -To create a graph from an adjecency matrix, use :meth:`Graph.Adjacency` or, for weighted matrices, :meth:`Graph.Weighted_Adjacency`: +To create a graph from an adjacency matrix, use :meth:`Graph.Adjacency` or, for weighted matrices, :meth:`Graph.Weighted_Adjacency`: >>> g = Graph.Adjacency([[0, 1, 1], [0, 0, 0], [0, 0, 1]]) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index d3cacd368..94541a63e 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -4,8 +4,7 @@ __license__ = """ -Copyright (C) 2006-2012 Tamás Nepusz -Pázmány Péter sétány 1/a, 1117 Budapest, Hungary +Copyright (C) 2006- The igraph development team 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 @@ -121,6 +120,53 @@ ) from igraph.datatypes import Matrix, DyadCensus, TriadCensus, UniqueIdGenerator from igraph.formula import construct_graph_from_formula +from igraph.io.files import ( + _construct_graph_from_graphmlz_file, + _construct_graph_from_dimacs_file, + _construct_graph_from_pickle_file, + _construct_graph_from_picklez_file, + _construct_graph_from_adjacency_file, + _construct_graph_from_file, + _write_graph_to_adjacency_file, + _write_graph_to_dimacs_file, + _write_graph_to_graphmlz_file, + _write_graph_to_pickle_file, + _write_graph_to_picklez_file, + _write_graph_to_file, +) +from igraph.io.objects import ( + _construct_graph_from_dict_list, + _export_graph_to_dict_list, + _construct_graph_from_tuple_list, + _export_graph_to_tuple_list, + _construct_graph_from_list_dict, + _export_graph_to_list_dict, + _construct_graph_from_dict_dict, + _export_graph_to_dict_dict, + _construct_graph_from_dataframe, + _export_vertex_dataframe, + _export_edge_dataframe, +) +from igraph.io.adjacency import ( + _construct_graph_from_adjacency, + _construct_graph_from_weighted_adjacency, +) +from igraph.io.libraries import ( + _construct_graph_from_networkx, + _export_graph_to_networkx, + _construct_graph_from_graph_tool, + _export_graph_to_graph_tool, +) +from igraph.io.random import ( + _construct_random_geometric_graph, +) +from igraph.io.bipartite import ( + _construct_bipartite_graph, + _construct_incidence_bipartite_graph, + _construct_full_bipartite_graph, + _construct_random_bipartite_graph, +) +from igraph.io.images import _write_graph_to_svg from igraph.layout import Layout from igraph.matching import Matching from igraph.operators import disjoint_union, union, intersection @@ -145,14 +191,8 @@ safemax, ) from igraph.version import __version__, __version_info__ -from igraph.sparse_matrix import ( - _graph_from_sparse_matrix, - _graph_from_weighted_sparse_matrix, -) import os -import math -import gzip import sys import operator @@ -1877,1339 +1917,76 @@ def maximum_bipartite_matching(self, types="type", weights=None, eps=None): ############################################# # Auxiliary I/O functions - def to_networkx(self, create_using=None): - """Converts the graph to networkx format. - - @param create_using: specifies which NetworkX graph class to use when - constructing the graph. C{None} means to let igraph infer the most - appropriate class based on whether the graph is directed and whether - it has multi-edges. - """ - import networkx as nx - - # Graph: decide on directness and mutliplicity - if create_using is None: - if self.has_multiple(): - cls = nx.MultiDiGraph if self.is_directed() else nx.MultiGraph - else: - cls = nx.DiGraph if self.is_directed() else nx.Graph - else: - cls = create_using - - # Graph attributes - kw = {x: self[x] for x in self.attributes()} - g = cls(**kw) - - # Nodes and node attributes - for i, v in enumerate(self.vs): - # TODO: use _nx_name if the attribute is present so we can achieve - # a lossless round-trip in terms of vertex names - g.add_node(i, **v.attributes()) - - # Edges and edge attributes - for edge in self.es: - g.add_edge(edge.source, edge.target, **edge.attributes()) - - return g - - @classmethod - def from_networkx(cls, g): - """Converts the graph from networkx - - Vertex names will be converted to "_nx_name" attribute and the vertices - will get new ids from 0 up (as standard in igraph). - - @param g: networkx Graph or DiGraph - """ - # Graph attributes - gattr = dict(g.graph) - - # Nodes - vnames = list(g.nodes) - vattr = {"_nx_name": vnames} - vcount = len(vnames) - vd = {v: i for i, v in enumerate(vnames)} - - # NOTE: we do not need a special class for multigraphs, it is taken - # care for at the edge level rather than at the graph level. - graph = cls( - n=vcount, directed=g.is_directed(), graph_attrs=gattr, vertex_attrs=vattr - ) - - # Node attributes - for v, datum in g.nodes.data(): - for key, val in list(datum.items()): - graph.vs[vd[v]][key] = val - - # Edges and edge attributes - eattr_names = {name for (_, _, data) in g.edges.data() for name in data} - eattr = {name: [] for name in eattr_names} - edges = [] - for (u, v, data) in g.edges.data(): - edges.append((vd[u], vd[v])) - for name in eattr_names: - eattr[name].append(data.get(name)) - - graph.add_edges(edges, eattr) - - return graph - - def to_graph_tool( - self, graph_attributes=None, vertex_attributes=None, edge_attributes=None - ): - """Converts the graph to graph-tool - - Data types: graph-tool only accepts specific data types. See the - following web page for a list: - - https://graph-tool.skewed.de/static/doc/quickstart.html - - Note: because of the restricted data types in graph-tool, vertex and - edge attributes require to be type-consistent across all vertices or - edges. If you set the property for only some vertices/edges, the other - will be tagged as None in python-igraph, so they can only be converted - to graph-tool with the type 'object' and any other conversion will - fail. - - @param graph_attributes: dictionary of graph attributes to transfer. - Keys are attributes from the graph, values are data types (see - below). C{None} means no graph attributes are transferred. - @param vertex_attributes: dictionary of vertex attributes to transfer. - Keys are attributes from the vertices, values are data types (see - below). C{None} means no vertex attributes are transferred. - @param edge_attributes: dictionary of edge attributes to transfer. - Keys are attributes from the edges, values are data types (see - below). C{None} means no vertex attributes are transferred. - """ - import graph_tool as gt - - # Graph - g = gt.Graph(directed=self.is_directed()) - - # Nodes - vc = self.vcount() - g.add_vertex(vc) - - # Graph attributes - if graph_attributes is not None: - for x, dtype in list(graph_attributes.items()): - # Strange syntax for setting internal properties - gprop = g.new_graph_property(str(dtype)) - g.graph_properties[x] = gprop - g.graph_properties[x] = self[x] - - # Vertex attributes - if vertex_attributes is not None: - for x, dtype in list(vertex_attributes.items()): - # Create a new vertex property - g.vertex_properties[x] = g.new_vertex_property(str(dtype)) - # Fill the values from the igraph.Graph - for i in range(vc): - g.vertex_properties[x][g.vertex(i)] = self.vs[i][x] - - # Edges and edge attributes - if edge_attributes is not None: - for x, dtype in list(edge_attributes.items()): - g.edge_properties[x] = g.new_edge_property(str(dtype)) - for edge in self.es: - e = g.add_edge(edge.source, edge.target) - if edge_attributes is not None: - for x, dtype in list(edge_attributes.items()): - prop = edge.attributes().get(x, None) - g.edge_properties[x][e] = prop - - return g - - @classmethod - def from_graph_tool(cls, g): - """Converts the graph from graph-tool - - @param g: graph-tool Graph - """ - # Graph attributes - gattr = dict(g.graph_properties) - - # Nodes - vcount = g.num_vertices() - - # Graph - graph = cls(n=vcount, directed=g.is_directed(), graph_attrs=gattr) - - # Node attributes - for key, val in g.vertex_properties.items(): - prop = val.get_array() - for i in range(vcount): - graph.vs[i][key] = prop[i] - - # Edges and edge attributes - # NOTE: graph-tool is quite strongly typed, so each property is always - # defined for all edges, using default values for the type. E.g. for a - # string property/attribute the missing edges get an empty string. - edges = [] - eattr_names = list(g.edge_properties) - eattr = {name: [] for name in eattr_names} - for e in g.edges(): - edges.append((int(e.source()), int(e.target()))) - for name, attr_map in g.edge_properties.items(): - eattr[name].append(attr_map[e]) - - graph.add_edges(edges, eattr) - - return graph - - def write_adjacency(self, f, sep=" ", eol="\n", *args, **kwds): - """Writes the adjacency matrix of the graph to the given file - - All the remaining arguments not mentioned here are passed intact - to L{Graph.get_adjacency}. - - @param f: the name of the file to be written. - @param sep: the string that separates the matrix elements in a row - @param eol: the string that separates the rows of the matrix. Please - note that igraph is able to read back the written adjacency matrix - if and only if this is a single newline character - """ - if isinstance(f, str): - f = open(f, "w") - matrix = self.get_adjacency(*args, **kwds) - for row in matrix: - f.write(sep.join(map(str, row))) - f.write(eol) - f.close() - - @classmethod - def Read_Adjacency( - cls, f, sep=None, comment_char="#", attribute=None, *args, **kwds - ): - """Constructs a graph based on an adjacency matrix from the given file. - - Additional positional and keyword arguments not mentioned here are - passed intact to L{Adjacency}. - - @param f: the name of the file to be read or a file object - @param sep: the string that separates the matrix elements in a row. - C{None} means an arbitrary sequence of whitespace characters. - @param comment_char: lines starting with this string are treated - as comments. - @param attribute: an edge attribute name where the edge weights are - stored in the case of a weighted adjacency matrix. If C{None}, - no weights are stored, values larger than 1 are considered as - edge multiplicities. - @return: the created graph""" - if isinstance(f, str): - f = open(f) - - matrix, ri = [], 0 - for line in f: - line = line.strip() - if len(line) == 0: - continue - if line.startswith(comment_char): - continue - row = [float(x) for x in line.split(sep)] - matrix.append(row) - ri += 1 - - f.close() - - if attribute is None: - graph = cls.Adjacency(matrix, *args, **kwds) - else: - kwds["attr"] = attribute - graph = cls.Weighted_Adjacency(matrix, *args, **kwds) - - return graph - - @classmethod - def Adjacency(cls, matrix, mode="directed", *args, **kwargs): - """Generates a graph from its adjacency matrix. - - @param matrix: the adjacency matrix. Possible types are: - - a list of lists - - a numpy 2D array or matrix (will be converted to list of lists) - - a scipy.sparse matrix (will be converted to a COO matrix, but not - to a dense matrix) - @param mode: the mode to be used. Possible values are: - - C{"directed"} - the graph will be directed and a matrix - element gives the number of edges between two vertex. - - C{"undirected"} - alias to C{"max"} for convenience. - - C{"max"} - undirected graph will be created and the number of - edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} - - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} - - C{"upper"} - undirected graph with the upper right triangle of - the matrix (including the diagonal) - - C{"lower"} - undirected graph with the lower left triangle of - the matrix (including the diagonal) - """ - try: - import numpy as np - except ImportError: - np = None - - try: - from scipy import sparse - except ImportError: - sparse = None - - if (sparse is not None) and isinstance(matrix, sparse.spmatrix): - return _graph_from_sparse_matrix(cls, matrix, mode=mode) - - if (np is not None) and isinstance(matrix, np.ndarray): - matrix = matrix.tolist() - - return super().Adjacency(matrix, mode=mode) + # Graph libraries + from_networkx = classmethod(_construct_graph_from_networkx) + to_networkx = _export_graph_to_networkx - @classmethod - def Weighted_Adjacency(cls, matrix, mode="directed", attr="weight", loops=True): - """Generates a graph from its weighted adjacency matrix. - - @param matrix: the adjacency matrix. Possible types are: - - a list of lists - - a numpy 2D array or matrix (will be converted to list of lists) - - a scipy.sparse matrix (will be converted to a COO matrix, but not - to a dense matrix) - @param mode: the mode to be used. Possible values are: - - C{"directed"} - the graph will be directed and a matrix - element gives the number of edges between two vertex. - - C{"undirected"} - alias to C{"max"} for convenience. - - C{"max"} - undirected graph will be created and the number of - edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} - - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} - - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} - - C{"upper"} - undirected graph with the upper right triangle of - the matrix (including the diagonal) - - C{"lower"} - undirected graph with the lower left triangle of - the matrix (including the diagonal) - - These values can also be given as strings without the C{ADJ} prefix. - @param attr: the name of the edge attribute that stores the edge - weights. - @param loops: whether to include loop edges. When C{False}, the diagonal - of the adjacency matrix will be ignored. - - """ - try: - import numpy as np - except ImportError: - np = None - - try: - from scipy import sparse - except ImportError: - sparse = None - - if sparse is not None and isinstance(matrix, sparse.spmatrix): - return _graph_from_weighted_sparse_matrix( - cls, - matrix, - mode=mode, - attr=attr, - loops=loops, - ) - - if np is not None and isinstance(matrix, np.ndarray): - matrix = matrix.tolist() + from_graph_tool = classmethod(_construct_graph_from_graph_tool) + to_graph_tool = _export_graph_to_graph_tool - return super().Weighted_Adjacency( - matrix, - mode=mode, - attr=attr, - loops=loops, - ) - - def write_dimacs(self, f, source=None, target=None, capacity="capacity"): - """Writes the graph in DIMACS format to the given file. - - @param f: the name of the file to be written or a Python file handle. - @param source: the source vertex ID. If C{None}, igraph will try to - infer it from the C{source} graph attribute. - @param target: the target vertex ID. If C{None}, igraph will try to - infer it from the C{target} graph attribute. - @param capacity: the capacities of the edges in a list or the name of - an edge attribute that holds the capacities. If there is no such - edge attribute, every edge will have a capacity of 1. - """ - if source is None: - try: - source = self["source"] - except KeyError: - raise ValueError( - "source vertex must be provided in the 'source' graph " - "attribute or in the 'source' argument of write_dimacs()" - ) - - if target is None: - try: - target = self["target"] - except KeyError: - raise ValueError( - "target vertex must be provided in the 'target' graph " - "attribute or in the 'target' argument of write_dimacs()" - ) + # Files + Read_DIMACS = classmethod(_construct_graph_from_dimacs_file) + write_dimacs = _write_graph_to_dimacs_file - if isinstance(capacity, str) and capacity not in self.edge_attributes(): - warn("'%s' edge attribute does not exist" % capacity) - capacity = [1] * self.ecount() + Read_GraphMLz = classmethod(_construct_graph_from_graphmlz_file) + write_graphmlz = _write_graph_to_graphmlz_file - return GraphBase.write_dimacs(self, f, source, target, capacity) + Read_Pickle = classmethod(_construct_graph_from_pickle_file) + write_pickle = _write_graph_to_pickle_file - def write_graphmlz(self, f, compresslevel=9): - """Writes the graph to a zipped GraphML file. + Read_Picklez = classmethod(_construct_graph_from_picklez_file) + write_picklez = _write_graph_to_picklez_file - The library uses the gzip compression algorithm, so the resulting - file can be unzipped with regular gzip uncompression (like - C{gunzip} or C{zcat} from Unix command line) or the Python C{gzip} - module. - - Uses a temporary file to store intermediate GraphML data, so - make sure you have enough free space to store the unzipped - GraphML file as well. - - @param f: the name of the file to be written. - @param compresslevel: the level of compression. 1 is fastest and - produces the least compression, and 9 is slowest and produces - the most compression.""" - with named_temporary_file() as tmpfile: - self.write_graphml(tmpfile) - outf = gzip.GzipFile(f, "wb", compresslevel) - copyfileobj(open(tmpfile, "rb"), outf) - outf.close() - - @classmethod - def Read_DIMACS(cls, f, directed=False): - """Reads a graph from a file conforming to the DIMACS minimum-cost flow - file format. - - For the exact definition of the format, see - U{http://lpsolve.sourceforge.net/5.5/DIMACS.htm}. - - Restrictions compared to the official description of the format are - as follows: - - - igraph's DIMACS reader requires only three fields in an arc - definition, describing the edge's source and target node and - its capacity. - - Source vertices are identified by 's' in the FLOW field, target - vertices are identified by 't'. - - Node indices start from 1. Only a single source and target node - is allowed. - - @param f: the name of the file or a Python file handle - @param directed: whether the generated graph should be directed. - @return: the generated graph. The indices of the source and target - vertices are attached as graph attributes C{source} and C{target}, - the edge capacities are stored in the C{capacity} edge attribute. - """ - graph, source, target, cap = super().Read_DIMACS(f, directed) - graph.es["capacity"] = cap - graph["source"] = source - graph["target"] = target - return graph - - @classmethod - def Read_GraphMLz(cls, f, directed=True, index=0): - """Reads a graph from a zipped GraphML file. - - @param f: the name of the file - @param index: if the GraphML file contains multiple graphs, - specified the one that should be loaded. Graph indices - start from zero, so if you want to load the first graph, - specify 0 here. - @return: the loaded graph object""" - with named_temporary_file() as tmpfile: - with open(tmpfile, "wb") as outf: - copyfileobj(gzip.GzipFile(f, "rb"), outf) - return cls.Read_GraphML(tmpfile, directed=directed, index=index) - - def write_pickle(self, fname=None, version=-1): - """Saves the graph in Python pickled format - - @param fname: the name of the file or a stream to save to. If - C{None}, saves the graph to a string and returns the string. - @param version: pickle protocol version to be used. If -1, uses - the highest protocol available - @return: C{None} if the graph was saved successfully to the - given file, or a string if C{fname} was C{None}. - """ - import pickle as pickle - - if fname is None: - return pickle.dumps(self, version) - if not hasattr(fname, "write"): - file_was_opened = True - fname = open(fname, "wb") - else: - file_was_opened = False - result = pickle.dump(self, fname, version) - if file_was_opened: - fname.close() - return result - - def write_picklez(self, fname=None, version=-1): - """Saves the graph in Python pickled format, compressed with - gzip. - - Saving in this format is a bit slower than saving in a Python pickle - without compression, but the final file takes up much less space on - the hard drive. - - @param fname: the name of the file or a stream to save to. - @param version: pickle protocol version to be used. If -1, uses - the highest protocol available - @return: C{None} if the graph was saved successfully to the - given file. - """ - import pickle as pickle - - file_was_opened = False - - if not hasattr(fname, "write"): - file_was_opened = True - fname = gzip.open(fname, "wb") - elif not isinstance(fname, gzip.GzipFile): - file_was_opened = True - fname = gzip.GzipFile(mode="wb", fileobj=fname) - - result = pickle.dump(self, fname, version) - - if file_was_opened: - fname.close() - - return result - - @classmethod - def Read_Pickle(cls, fname=None): - """Reads a graph from Python pickled format - - @param fname: the name of the file, a stream to read from, or - a string containing the pickled data. - @return: the created graph object. - """ - import pickle as pickle - - if hasattr(fname, "read"): - # Probably a file or a file-like object - result = pickle.load(fname) - else: - try: - fp = open(fname, "rb") - except UnicodeDecodeError: - try: - # We are on Python 3.6 or above and we are passing a pickled - # stream that cannot be decoded as Unicode. Try unpickling - # directly. - result = pickle.loads(fname) - except TypeError: - raise IOError( - "Cannot load file. If fname is a file name, that " - "filename may be incorrect." - ) - except IOError: - try: - # No file with the given name, try unpickling directly. - result = pickle.loads(fname) - except TypeError: - raise IOError( - "Cannot load file. If fname is a file name, that " - "filename may be incorrect." - ) - else: - result = pickle.load(fp) - fp.close() - - if not isinstance(result, cls): - raise TypeError("unpickled object is not a %s" % cls.__name__) - - return result - - @classmethod - def Read_Picklez(cls, fname): - """Reads a graph from compressed Python pickled format, uncompressing - it on-the-fly. - - @param fname: the name of the file or a stream to read from. - @return: the created graph object. - """ - import pickle as pickle - - if hasattr(fname, "read"): - # Probably a file or a file-like object - if isinstance(fname, gzip.GzipFile): - result = pickle.load(fname) - else: - result = pickle.load(gzip.GzipFile(mode="rb", fileobj=fname)) - else: - result = pickle.load(gzip.open(fname, "rb")) - - if not isinstance(result, cls): - raise TypeError("unpickled object is not a %s" % cls.__name__) - - return result - - def write_svg( - self, - fname, - layout="auto", - width=None, - height=None, - labels="label", - colors="color", - shapes="shape", - vertex_size=10, - edge_colors="color", - edge_stroke_widths="width", - font_size=16, - *args, - **kwds, - ): - """Saves the graph as an SVG (Scalable Vector Graphics) file - - The file will be Inkscape (http://inkscape.org) compatible. - In Inkscape, as nodes are rearranged, the edges auto-update. - - @param fname: the name of the file or a Python file handle - @param layout: the layout of the graph. Can be either an - explicitly specified layout (using a list of coordinate - pairs) or the name of a layout algorithm (which should - refer to a method in the L{Graph} object, but without - the C{layout_} prefix. - @param width: the preferred width in pixels (default: 400) - @param height: the preferred height in pixels (default: 400) - @param labels: the vertex labels. Either it is the name of - a vertex attribute to use, or a list explicitly specifying - the labels. It can also be C{None}. - @param colors: the vertex colors. Either it is the name of - a vertex attribute to use, or a list explicitly specifying - the colors. A color can be anything acceptable in an SVG - file. - @param shapes: the vertex shapes. Either it is the name of - a vertex attribute to use, or a list explicitly specifying - the shapes as integers. Shape 0 means hidden (nothing is drawn), - shape 1 is a circle, shape 2 is a rectangle and shape 3 is a - rectangle that automatically sizes to the inner text. - @param vertex_size: vertex size in pixels - @param edge_colors: the edge colors. Either it is the name - of an edge attribute to use, or a list explicitly specifying - the colors. A color can be anything acceptable in an SVG - file. - @param edge_stroke_widths: the stroke widths of the edges. Either - it is the name of an edge attribute to use, or a list explicitly - specifying the stroke widths. The stroke width can be anything - acceptable in an SVG file. - @param font_size: font size. If it is a string, it is written into - the SVG file as-is (so you can specify anything which is valid - as the value of the C{font-size} style). If it is a number, it - is interpreted as pixel size and converted to the proper attribute - value accordingly. - """ - if width is None and height is None: - width = 400 - height = 400 - elif width is None: - width = height - elif height is None: - height = width - - if width <= 0 or height <= 0: - raise ValueError("width and height must be positive") - - if isinstance(layout, str): - layout = self.layout(layout, *args, **kwds) - - if isinstance(labels, str): - try: - labels = self.vs.get_attribute_values(labels) - except KeyError: - labels = [x + 1 for x in range(self.vcount())] - elif labels is None: - labels = [""] * self.vcount() - - if isinstance(colors, str): - try: - colors = self.vs.get_attribute_values(colors) - except KeyError: - colors = ["red"] * self.vcount() - - if isinstance(shapes, str): - try: - shapes = self.vs.get_attribute_values(shapes) - except KeyError: - shapes = [1] * self.vcount() - - if isinstance(edge_colors, str): - try: - edge_colors = self.es.get_attribute_values(edge_colors) - except KeyError: - edge_colors = ["black"] * self.ecount() - - if isinstance(edge_stroke_widths, str): - try: - edge_stroke_widths = self.es.get_attribute_values(edge_stroke_widths) - except KeyError: - edge_stroke_widths = [2] * self.ecount() - - if not isinstance(font_size, str): - font_size = "%spx" % str(font_size) - else: - if ";" in font_size: - raise ValueError("font size can't contain a semicolon") - - vcount = self.vcount() - labels.extend(str(i + 1) for i in range(len(labels), vcount)) - colors.extend(["red"] * (vcount - len(colors))) - - if isinstance(fname, str): - f = open(fname, "w") - our_file = True - else: - f = fname - our_file = False - - bbox = BoundingBox(layout.bounding_box()) - - sizes = [width - 2 * vertex_size, height - 2 * vertex_size] - w, h = bbox.width, bbox.height - - ratios = [] - if w == 0: - ratios.append(1.0) - else: - ratios.append(sizes[0] / w) - if h == 0: - ratios.append(1.0) - else: - ratios.append(sizes[1] / h) - - layout = [ - [ - (row[0] - bbox.left) * ratios[0] + vertex_size, - (row[1] - bbox.top) * ratios[1] + vertex_size, - ] - for row in layout - ] - - directed = self.is_directed() - - print('', file=f) - print( - "", - file=f, - ) - print(file=f) - print( - ''.format(width, height), end=" ", file=f) - - edge_color_dict = {} - print('', file=f) - for e_col in set(edge_colors): - if e_col == "#000000": - marker_index = "" - else: - marker_index = str(len(edge_color_dict)) - # Print an arrow marker for each possible line color - # This is a copy of Inkscape's standard Arrow 2 marker - print("', file=f) - print(" ', file=f) - print("", file=f) - - edge_color_dict[e_col] = "Arrow2Lend{0}".format(marker_index) - print("", file=f) - print( - '', - file=f, - ) - - for eidx, edge in enumerate(self.es): - vidxs = edge.tuple - x1 = layout[vidxs[0]][0] - y1 = layout[vidxs[0]][1] - x2 = layout[vidxs[1]][0] - y2 = layout[vidxs[1]][1] - angle = math.atan2(y2 - y1, x2 - x1) - x2 = x2 - vertex_size * math.cos(angle) - y2 = y2 - vertex_size * math.sin(angle) - - print("', file=f) - - print(" ", file=f) - print(file=f) - - print( - ' ', - file=f, - ) - print(" ", file=f) - - if any(x == 3 for x in shapes): - # Only import tkFont if we really need it. Unfortunately, this will - # flash up an unneccesary Tk window in some cases - import tkinter.font - import tkinter as tk - - # This allows us to dynamically size the width of the nodes. - # Unfortunately this works only with font sizes specified in pixels. - if font_size.endswith("px"): - font_size_in_pixels = int(font_size[:-2]) - else: - try: - font_size_in_pixels = int(font_size) - except Exception: - raise ValueError( - "font sizes must be specified in pixels " - "when any of the nodes has shape=3 (i.e. " - "node size determined by text size)" - ) - tk_window = tk.Tk() - font = tkinter.font.Font( - root=tk_window, font=("Sans", font_size_in_pixels, tkinter.font.NORMAL) - ) - else: - tk_window = None - - for vidx in range(self.vcount()): - print( - ' '.format( - vidx, layout[vidx][0], layout[vidx][1] - ), - file=f, - ) - if shapes[vidx] == 1: - # Undocumented feature: can handle two colors but only for circles - c = str(colors[vidx]) - if " " in c: - c = c.split(" ") - vs = str(vertex_size) - print( - ' '.format( - vs, c[0] - ), - file=f, - ) - print( - ' '.format( - vs, c[1] - ), - file=f, - ) - print( - ' '.format(vs), - file=f, - ) - else: - print( - ' '.format( - str(vertex_size), str(colors[vidx]) - ), - file=f, - ) - elif shapes[vidx] == 2: - print( - ' '.format( - vertex_size, vertex_size * 2, vidx, colors[vidx] - ), - file=f, - ) - elif shapes[vidx] == 3: - (vertex_width, vertex_height) = ( - font.measure(str(labels[vidx])) + 2, - font.metrics("linespace") + 2, - ) - print( - ' ".format( - vertex_width / 2.0, - vertex_height / 2.0, - vertex_width, - vertex_height, - vidx, - colors[vidx], - ), - file=f, - ) - - print( - ' '.format(vertex_size / 2.0, vidx, font_size), - file=f, - ) - print( - '' - "{2}".format(vertex_size / 2.0, vidx, str(labels[vidx])), - file=f, - ) - print(" ", file=f) - - print("", file=f) - print(file=f) - print("", file=f) - - if our_file: - f.close() - if tk_window: - tk_window.destroy() - - @classmethod - def _identify_format(cls, filename): - """_identify_format(filename) - - Tries to identify the format of the graph stored in the file with the - given filename. It identifies most file formats based on the extension - of the file (and not on syntactic evaluation). The only exception is - the adjacency matrix format and the edge list format: the first few - lines of the file are evaluated to decide between the two. - - @note: Internal function, should not be called directly. - - @param filename: the name of the file or a file object whose C{name} - attribute is set. - @return: the format of the file as a string. - """ - import os.path - - if hasattr(filename, "name") and hasattr(filename, "read"): - # It is most likely a file - try: - filename = filename.name - except Exception: - return None - - root, ext = os.path.splitext(filename) - ext = ext.lower() - - if ext == ".gz": - _, ext2 = os.path.splitext(root) - ext2 = ext2.lower() - if ext2 == ".pickle": - return "picklez" - elif ext2 == ".graphml": - return "graphmlz" - - if ext in [ - ".graphml", - ".graphmlz", - ".lgl", - ".ncol", - ".pajek", - ".gml", - ".dimacs", - ".edgelist", - ".edges", - ".edge", - ".net", - ".pickle", - ".picklez", - ".dot", - ".gw", - ".lgr", - ".dl", - ]: - return ext[1:] - - if ext == ".txt" or ext == ".dat": - # Most probably an adjacency matrix or an edge list - f = open(filename, "r") - line = f.readline() - if line is None: - return "edges" - parts = line.strip().split() - if len(parts) == 2: - line = f.readline() - if line is None: - return "edges" - parts = line.strip().split() - if len(parts) == 2: - line = f.readline() - if line is None: - # This is a 2x2 matrix, it can be a matrix or an edge - # list as well and we cannot decide - return None - else: - parts = line.strip().split() - if len(parts) == 0: - return None - return "edges" - else: - # Not a matrix - return None - else: - return "adjacency" - - @classmethod - def Read(cls, f, format=None, *args, **kwds): - """Unified reading function for graphs. - - This method tries to identify the format of the graph given in - the first parameter and calls the corresponding reader method. - - The remaining arguments are passed to the reader method without - any changes. - - @param f: the file containing the graph to be loaded - @param format: the format of the file (if known in advance). - C{None} means auto-detection. Possible values are: C{"ncol"} - (NCOL format), C{"lgl"} (LGL format), C{"graphdb"} (GraphDB - format), C{"graphml"}, C{"graphmlz"} (GraphML and gzipped - GraphML format), C{"gml"} (GML format), C{"net"}, C{"pajek"} - (Pajek format), C{"dimacs"} (DIMACS format), C{"edgelist"}, - C{"edges"} or C{"edge"} (edge list), C{"adjacency"} - (adjacency matrix), C{"dl"} (DL format used by UCINET), - C{"pickle"} (Python pickled format), - C{"picklez"} (gzipped Python pickled format) - @raises IOError: if the file format can't be identified and - none was given. - """ - if format is None: - format = cls._identify_format(f) - try: - reader = cls._format_mapping[format][0] - except (KeyError, IndexError): - raise IOError("unknown file format: %s" % str(format)) - if reader is None: - raise IOError("no reader method for file format: %s" % str(format)) - reader = getattr(cls, reader) - return reader(f, *args, **kwds) + Read_Adjacency = classmethod(_construct_graph_from_adjacency_file) + write_adjacency = _write_graph_to_adjacency_file + Read = classmethod(_construct_graph_from_file) Load = Read + write = _write_graph_to_file + save = write - def write(self, f, format=None, *args, **kwds): - """Unified writing function for graphs. - - This method tries to identify the format of the graph given in - the first parameter (based on extension) and calls the corresponding - writer method. - - The remaining arguments are passed to the writer method without - any changes. - - @param f: the file containing the graph to be saved - @param format: the format of the file (if one wants to override the - format determined from the filename extension, or the filename itself - is a stream). C{None} means auto-detection. Possible values are: - - - C{"adjacency"}: adjacency matrix format - - - C{"dimacs"}: DIMACS format + # Various objects + # list of dict representation of graphs + DictList = classmethod(_construct_graph_from_dict_list) + to_dict_list = _export_graph_to_dict_list - - C{"dot"}, C{"graphviz"}: GraphViz DOT format + # tuple-like representation of graphs + TupleList = classmethod(_construct_graph_from_tuple_list) + to_tuple_list = _export_graph_to_tuple_list - - C{"edgelist"}, C{"edges"} or C{"edge"}: numeric edge list format + # dict of sequence representation of graphs + ListDict = classmethod(_construct_graph_from_list_dict) + to_list_dict = _export_graph_to_list_dict - - C{"gml"}: GML format + # dict of dicts representation of graphs + DictDict = classmethod(_construct_graph_from_dict_dict) + to_dict_dict = _export_graph_to_dict_dict - - C{"graphml"} and C{"graphmlz"}: standard and gzipped GraphML - format + # adjacency matrix + Adjacency = classmethod(_construct_graph_from_adjacency) - - C{"gw"}, C{"leda"}, C{"lgr"}: LEDA native format + Weighted_Adjacency = classmethod(_construct_graph_from_weighted_adjacency) - - C{"lgl"}: LGL format + # pandas dataframe(s) + DataFrame = classmethod(_construct_graph_from_dataframe) - - C{"ncol"}: NCOL format + get_vertex_dataframe = _export_vertex_dataframe - - C{"net"}, C{"pajek"}: Pajek format + get_edge_dataframe = _export_edge_dataframe - - C{"pickle"}, C{"picklez"}: standard and gzipped Python pickled - format + # Bipartite graphs + Bipartite = classmethod(_construct_bipartite_graph) - - C{"svg"}: SVG format + Incidence = classmethod(_construct_incidence_bipartite_graph) - @raises IOError: if the file format can't be identified and - none was given. - """ - if format is None: - format = self._identify_format(f) - try: - writer = self._format_mapping[format][1] - except (KeyError, IndexError): - raise IOError("unknown file format: %s" % str(format)) - if writer is None: - raise IOError("no writer method for file format: %s" % str(format)) - writer = getattr(self, writer) - return writer(f, *args, **kwds) + Full_Bipartite = classmethod(_construct_full_bipartite_graph) - save = write + Random_Bipartite = classmethod(_construct_random_bipartite_graph) - ##################################################### - # Constructor for dict-like representation of graphs + # Other constructors + GRG = classmethod(_construct_random_geometric_graph) - @classmethod - def DictList( - cls, - vertices, - edges, - directed=False, - vertex_name_attr="name", - edge_foreign_keys=("source", "target"), - iterative=False, - ): - """Constructs a graph from a list-of-dictionaries representation. - - This representation assumes that vertices and edges are encoded in - two lists, each list containing a Python dict for each vertex and - each edge, respectively. A distinguished element of the vertex dicts - contain a vertex ID which is used in the edge dicts to refer to - source and target vertices. All the remaining elements of the dict - are considered vertex and edge attributes. Note that the implementation - does not assume that the objects passed to this method are indeed - lists of dicts, but they should be iterable and they should yield - objects that behave as dicts. So, for instance, a database query - result is likely to be fit as long as it's iterable and yields - dict-like objects with every iteration. - - @param vertices: the data source for the vertices or C{None} if - there are no special attributes assigned to vertices and we - should simply use the edge list of dicts to infer vertex names. - @param edges: the data source for the edges. - @param directed: whether the constructed graph will be directed - @param vertex_name_attr: the name of the distinguished key in the - dicts in the vertex data source that contains the vertex names. - Ignored if C{vertices} is C{None}. - @param edge_foreign_keys: the name of the attributes in the dicts - in the edge data source that contain the source and target - vertex names. - @param iterative: whether to add the edges to the graph one by one, - iteratively, or to build a large edge list first and use that to - construct the graph. The latter approach is faster but it may - not be suitable if your dataset is large. The default is to - add the edges in a batch from an edge list. - @return: the graph that was constructed - """ - - def create_list_from_indices(indices, n): - result = [None] * n - for i, v in indices: - result[i] = v - return result - - # Construct the vertices - vertex_attrs, n = {}, 0 - if vertices: - for idx, vertex_data in enumerate(vertices): - for k, v in vertex_data.items(): - try: - vertex_attrs[k].append((idx, v)) - except KeyError: - vertex_attrs[k] = [(idx, v)] - n += 1 - for k, v in vertex_attrs.items(): - vertex_attrs[k] = create_list_from_indices(v, n) - else: - vertex_attrs[vertex_name_attr] = [] - - vertex_names = vertex_attrs[vertex_name_attr] - # Check for duplicates in vertex_names - if len(vertex_names) != len(set(vertex_names)): - raise ValueError("vertex names are not unique") - # Create a reverse mapping from vertex names to indices - vertex_name_map = UniqueIdGenerator(initial=vertex_names) - - # Construct the edges - efk_src, efk_dest = edge_foreign_keys - if iterative: - g = cls(n, [], directed, {}, vertex_attrs) - for idx, edge_data in enumerate(edges): - src_name, dst_name = edge_data[efk_src], edge_data[efk_dest] - v1 = vertex_name_map[src_name] - if v1 == n: - g.add_vertices(1) - g.vs[n][vertex_name_attr] = src_name - n += 1 - v2 = vertex_name_map[dst_name] - if v2 == n: - g.add_vertices(1) - g.vs[n][vertex_name_attr] = dst_name - n += 1 - g.add_edge(v1, v2) - for k, v in edge_data.items(): - g.es[idx][k] = v - - return g - else: - edge_list, edge_attrs, m = [], {}, 0 - for idx, edge_data in enumerate(edges): - v1 = vertex_name_map[edge_data[efk_src]] - v2 = vertex_name_map[edge_data[efk_dest]] - - edge_list.append((v1, v2)) - for k, v in edge_data.items(): - try: - edge_attrs[k].append((idx, v)) - except KeyError: - edge_attrs[k] = [(idx, v)] - m += 1 - for k, v in edge_attrs.items(): - edge_attrs[k] = create_list_from_indices(v, m) - - # It may have happened that some vertices were added during - # the process - if len(vertex_name_map) > n: - diff = len(vertex_name_map) - n - more = [None] * diff - for k, v in vertex_attrs.items(): - v.extend(more) - vertex_attrs[vertex_name_attr] = list(vertex_name_map.values()) - n = len(vertex_name_map) - - # Create the graph - return cls(n, edge_list, directed, {}, vertex_attrs, edge_attrs) - - ##################################################### - # Constructor for tuple-like representation of graphs - - @classmethod - def TupleList( - cls, - edges, - directed=False, - vertex_name_attr="name", - edge_attrs=None, - weights=False, - ): - """Constructs a graph from a list-of-tuples representation. - - This representation assumes that the edges of the graph are encoded - in a list of tuples (or lists). Each item in the list must have at least - two elements, which specify the source and the target vertices of the edge. - The remaining elements (if any) specify the edge attributes of that edge, - where the names of the edge attributes originate from the C{edge_attrs} - list. The names of the vertices will be stored in the vertex attribute - given by C{vertex_name_attr}. - - The default parameters of this function are suitable for creating - unweighted graphs from lists where each item contains the source vertex - and the target vertex. If you have a weighted graph, you can use items - where the third item contains the weight of the edge by setting - C{edge_attrs} to C{"weight"} or C{["weight"]}. If you have even more - edge attributes, add them to the end of each item in the C{edges} - list and also specify the corresponding edge attribute names in - C{edge_attrs} as a list. - - @param edges: the data source for the edges. This must be a list - where each item is a tuple (or list) containing at least two - items: the name of the source and the target vertex. Note that - names will be assigned to the C{name} vertex attribute (or another - vertex attribute if C{vertex_name_attr} is specified), even if - all the vertex names in the list are in fact numbers. - @param directed: whether the constructed graph will be directed - @param vertex_name_attr: the name of the vertex attribute that will - contain the vertex names. - @param edge_attrs: the names of the edge attributes that are filled - with the extra items in the edge list (starting from index 2, since - the first two items are the source and target vertices). C{None} - means that only the source and target vertices will be extracted - from each item. If you pass a string here, it will be wrapped in - a list for convenience. - @param weights: alternative way to specify that the graph is - weighted. If you set C{weights} to C{true} and C{edge_attrs} is - not given, it will be assumed that C{edge_attrs} is C{["weight"]} - and igraph will parse the third element from each item into an - edge weight. If you set C{weights} to a string, it will be assumed - that C{edge_attrs} contains that string only, and igraph will - store the edge weights in that attribute. - @return: the graph that was constructed - """ - if edge_attrs is None: - if not weights: - edge_attrs = () - else: - if not isinstance(weights, str): - weights = "weight" - edge_attrs = [weights] - else: - if weights: - raise ValueError( - "`weights` must be False if `edge_attrs` is " "not None" - ) - - if isinstance(edge_attrs, str): - edge_attrs = [edge_attrs] - - # Set up a vertex ID generator - idgen = UniqueIdGenerator() - - # Construct the edges and the edge attributes - edge_list = [] - edge_attributes = {} - for name in edge_attrs: - edge_attributes[name] = [] - - for item in edges: - edge_list.append((idgen[item[0]], idgen[item[1]])) - for index, name in enumerate(edge_attrs, 2): - try: - edge_attributes[name].append(item[index]) - except IndexError: - edge_attributes[name].append(None) - - # Set up the "name" vertex attribute - vertex_attributes = {} - vertex_attributes[vertex_name_attr] = list(idgen.values()) - n = len(idgen) - - # Construct the graph - return cls(n, edge_list, directed, {}, vertex_attributes, edge_attributes) - - ################################# - # Constructor for graph formulae + # Graph formulae Formula = classmethod(construct_graph_from_formula) ########################### @@ -3225,390 +2002,6 @@ def es(self): """The edge sequence of the graph""" return EdgeSeq(self) - ############################################# - # Friendlier interface for bipartite methods - - @classmethod - def Bipartite(cls, types, edges, directed=False, *args, **kwds): - """Creates a bipartite graph with the given vertex types and edges. - This is similar to the default constructor of the graph, the - only difference is that it checks whether all the edges go - between the two vertex classes and it assigns the type vector - to a C{type} attribute afterwards. - - Examples: - - >>> g = Graph.Bipartite([0, 1, 0, 1], [(0, 1), (2, 3), (0, 3)]) - >>> g.is_bipartite() - True - >>> g.vs["type"] - [False, True, False, True] - - @param types: the vertex types as a boolean list. Anything that - evaluates to C{False} will denote a vertex of the first kind, - anything that evaluates to C{True} will denote a vertex of the - second kind. - @param edges: the edges as a list of tuples. - @param directed: whether to create a directed graph. Bipartite - networks are usually undirected, so the default is C{False} - - @return: the graph with a binary vertex attribute named C{"type"} that - stores the vertex classes. - """ - result = cls._Bipartite(types, edges, directed, *args, **kwds) - result.vs["type"] = [bool(x) for x in types] - return result - - @classmethod - def Full_Bipartite(cls, n1, n2, directed=False, mode="all", *args, **kwds): - """Generates a full bipartite graph (directed or undirected, with or - without loops). - - >>> g = Graph.Full_Bipartite(2, 3) - >>> g.is_bipartite() - True - >>> g.vs["type"] - [False, False, True, True, True] - - @param n1: the number of vertices of the first kind. - @param n2: the number of vertices of the second kind. - @param directed: whether tp generate a directed graph. - @param mode: if C{"out"}, then all vertices of the first kind are - connected to the others; C{"in"} specifies the opposite direction, - C{"all"} creates mutual edges. Ignored for undirected graphs. - - @return: the graph with a binary vertex attribute named C{"type"} that - stores the vertex classes. - """ - result, types = cls._Full_Bipartite(n1, n2, directed, mode, *args, **kwds) - result.vs["type"] = types - return result - - @classmethod - def Random_Bipartite( - cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds - ): - """Generates a random bipartite graph with the given number of vertices and - edges (if m is given), or with the given number of vertices and the given - connection probability (if p is given). - - If m is given but p is not, the generated graph will have n1 vertices of - type 1, n2 vertices of type 2 and m randomly selected edges between them. If - p is given but m is not, the generated graph will have n1 vertices of type 1 - and n2 vertices of type 2, and each edge will exist between them with - probability p. - - @param n1: the number of vertices of type 1. - @param n2: the number of vertices of type 2. - @param p: the probability of edges. If given, C{m} must be missing. - @param m: the number of edges. If given, C{p} must be missing. - @param directed: whether to generate a directed graph. - @param neimode: if the graph is directed, specifies how the edges will be - generated. If it is C{"all"}, edges will be generated in both directions - (from type 1 to type 2 and vice versa) independently. If it is C{"out"} - edges will always point from type 1 to type 2. If it is C{"in"}, edges - will always point from type 2 to type 1. This argument is ignored for - undirected graphs. - """ - if p is None: - p = -1 - if m is None: - m = -1 - result, types = cls._Random_Bipartite( - n1, n2, p, m, directed, neimode, *args, **kwds - ) - result.vs["type"] = types - return result - - @classmethod - def GRG(cls, n, radius, torus=False): - """Generates a random geometric graph. - - The algorithm drops the vertices randomly on the 2D unit square and - connects them if they are closer to each other than the given radius. - The coordinates of the vertices are stored in the vertex attributes C{x} - and C{y}. - - @param n: The number of vertices in the graph - @param radius: The given radius - @param torus: This should be C{True} if we want to use a torus instead of a - square. - """ - result, xs, ys = cls._GRG(n, radius, torus) - result.vs["x"] = xs - result.vs["y"] = ys - return result - - @classmethod - def Incidence( - cls, - matrix, - directed=False, - mode="out", - multiple=False, - weighted=None, - *args, - **kwds, - ): - """Creates a bipartite graph from an incidence matrix. - - Example: - - >>> g = Graph.Incidence([[0, 1, 1], [1, 1, 0]]) - - @param matrix: the incidence matrix. - @param directed: whether to create a directed graph. - @param mode: defines the direction of edges in the graph. If - C{"out"}, then edges go from vertices of the first kind - (corresponding to rows of the matrix) to vertices of the - second kind (the columns of the matrix). If C{"in"}, the - opposite direction is used. C{"all"} creates mutual edges. - Ignored for undirected graphs. - @param multiple: defines what to do with non-zero entries in the - matrix. If C{False}, non-zero entries will create an edge no matter - what the value is. If C{True}, non-zero entries are rounded up to - the nearest integer and this will be the number of multiple edges - created. - @param weighted: defines whether to create a weighted graph from the - incidence matrix. If it is c{None} then an unweighted graph is created - and the multiple argument is used to determine the edges of the graph. - If it is a string then for every non-zero matrix entry, an edge is created - and the value of the entry is added as an edge attribute named by the - weighted argument. If it is C{True} then a weighted graph is created and - the name of the edge attribute will be ‘weight’. - - @raise ValueError: if the weighted and multiple are passed together. - - @return: the graph with a binary vertex attribute named C{"type"} that - stores the vertex classes. - """ - is_weighted = True if weighted or weighted == "" else False - if is_weighted and multiple: - raise ValueError("arguments weighted and multiple can not co-exist") - result, types = cls._Incidence(matrix, directed, mode, multiple, *args, **kwds) - result.vs["type"] = types - if is_weighted: - weight_attr = "weight" if weighted is True else weighted - _, rows, _ = result.get_incidence() - num_vertices_of_first_kind = len(rows) - for edge in result.es: - source, target = edge.tuple - if source in rows: - edge[weight_attr] = matrix[source][ - target - num_vertices_of_first_kind - ] - else: - edge[weight_attr] = matrix[target][ - source - num_vertices_of_first_kind - ] - return result - - @classmethod - def DataFrame(cls, edges, directed=True, vertices=None, use_vids=True): - """Generates a graph from one or two dataframes. - - @param edges: pandas DataFrame containing edges and metadata. The first - two columns of this DataFrame contain the source and target vertices - for each edge. These indicate the vertex IDs as nonnegative integers - rather than vertex *names* unless `use_vids` is False. Further columns - may contain edge attributes. - @param directed: bool setting whether the graph is directed - @param vertices: None (default) or pandas DataFrame containing vertex - metadata. The DataFrame's index must contain the vertex IDs as a - sequence of intergers from `0` to `len(vertices) - 1`. If `use_vids` - is False, the first column must contain the unique vertex *names*. - Although vertex names are usually strings, they can be any hashable - object. All other columns will be added as vertex attributes by column - name. - @use_vids: whether to interpret the first two columns of the `edges` - argument as vertex ids (0-based integers) instead of vertex names. - If this argument is set to True and the first two columns of `edges` - are not integers, an error is thrown. - - @return: the graph - - Vertex names in either the `edges` or `vertices` arguments that are set - to NaN (not a number) will be set to the string "NA". That might lead - to unexpected behaviour: fill your NaNs with values before calling this - function to mitigate. - """ - try: - import pandas as pd - except ImportError: - raise ImportError("You should install pandas in order to use this function") - try: - import numpy as np - except: - raise ImportError("You should install numpy in order to use this function") - - if edges.shape[1] < 2: - raise ValueError("The 'edges' DataFrame must contain at least two columns") - if vertices is not None and vertices.shape[1] < 1: - raise ValueError( - "The 'vertices' DataFrame must contain at least one column" - ) - - if use_vids: - if not ( - str(edges.dtypes[0]).startswith("int") - and str(edges.dtypes[1]).startswith("int") - ): - raise TypeError( - f"Source and target IDs must be 0-based integers, found types {edges.dtypes.tolist()[:2]}" - ) - elif (edges.iloc[:, :2] < 0).any(axis=None): - raise ValueError("Source and target IDs must not be negative") - if vertices is not None: - vertices = vertices.sort_index() - if not vertices.index.equals( - pd.RangeIndex.from_range(range(vertices.shape[0])) - ): - if not str(vertices.index.dtype).startswith("int"): - raise TypeError( - f"Vertex IDs must be 0-based integers, found type {vertices.index.dtype}" - ) - elif (vertices.index < 0).any(axis=None): - raise ValueError("Vertex IDs must not be negative") - else: - raise ValueError( - f"Vertex IDs must be an integer sequence from 0 to {vertices.shape[0] - 1}" - ) - else: - # Handle if some source and target names in 'edges' are 'NA' - if edges.iloc[:, :2].isna().any(axis=None): - warn( - "In the first two columns of 'edges' NA elements were replaced with string \"NA\"" - ) - edges = edges.copy() - edges.iloc[:, :2].fillna("NA", inplace=True) - - # Bring DataFrame(s) into same format as with 'use_vids=True' - if vertices is None: - vertices = pd.DataFrame({"name": np.unique(edges.values[:, :2])}) - - if vertices.iloc[:, 0].isna().any(): - warn( - "In the first column of 'vertices' NA elements were replaced with string \"NA\"" - ) - vertices = vertices.copy() - vertices.iloc[:, 0].fillna("NA", inplace=True) - - if vertices.iloc[:, 0].duplicated().any(): - raise ValueError("Vertex names must be unique") - - if vertices.shape[1] > 1 and "name" in vertices.columns[1:]: - raise ValueError( - "Vertex attribute conflict: DataFrame already contains column 'name'" - ) - - vertices = vertices.rename( - {vertices.columns[0]: "name"}, axis=1 - ).reset_index(drop=True) - - # Map source and target names in 'edges' to IDs - vid_map = pd.Series(vertices.index, index=vertices.iloc[:, 0]) - edges = edges.copy() - edges.iloc[:, 0] = edges.iloc[:, 0].map(vid_map) - edges.iloc[:, 1] = edges.iloc[:, 1].map(vid_map) - - # Create graph - if vertices is None: - nv = edges.iloc[:, :2].max().max() + 1 - g = Graph(n=nv, directed=directed) - else: - if not edges.iloc[:, :2].isin(vertices.index).all(axis=None): - raise ValueError( - "Some vertices in the edge DataFrame are missing from vertices DataFrame" - ) - nv = vertices.shape[0] - g = Graph(n=nv, directed=directed) - # Add vertex attributes - for col in vertices.columns: - g.vs[col] = vertices[col].tolist() - - # add edges including optional attributes - e_list = list(edges.iloc[:, :2].itertuples(index=False, name=None)) - e_attr = ( - edges.iloc[:, 2:].to_dict(orient="list") if edges.shape[1] > 2 else None - ) - g.add_edges(e_list, e_attr) - - return g - - def get_vertex_dataframe(self): - """Export vertices with attributes to pandas.DataFrame - - If you want to use vertex names as index, you can do: - - >>> from string import ascii_letters - >>> graph = Graph.GRG(25, 0.4) - >>> graph.vs["name"] = ascii_letters[:graph.vcount()] - >>> df = graph.get_vertex_dataframe() - >>> df.set_index('name', inplace=True) - - @return: a pandas.DataFrame representing vertices and their attributes. - The index uses vertex IDs, from 0 to N - 1 where N is the number of - vertices. - """ - try: - import pandas as pd - except ImportError: - raise ImportError("You should install pandas in order to use this function") - - df = pd.DataFrame( - {attr: self.vs[attr] for attr in self.vertex_attributes()}, - index=list(range(self.vcount())), - ) - df.index.name = "vertex ID" - - return df - - def get_edge_dataframe(self): - """Export edges with attributes to pandas.DataFrame - - If you want to use source and target vertex IDs as index, you can do: - - >>> from string import ascii_letters - >>> graph = Graph.GRG(25, 0.4) - >>> graph.vs["name"] = ascii_letters[:graph.vcount()] - >>> df = graph.get_edge_dataframe() - >>> df.set_index(['source', 'target'], inplace=True) - - The index will be a pandas.MultiIndex. You can use the `drop=False` - option to keep the `source` and `target` columns. - - If you want to use vertex names in the source and target columns: - - >>> df = graph.get_edge_dataframe() - >>> df_vert = graph.get_vertex_dataframe() - >>> df['source'].replace(df_vert['name'], inplace=True) - >>> df['target'].replace(df_vert['name'], inplace=True) - >>> df_vert.set_index('name', inplace=True) # Optional - - @return: a pandas.DataFrame representing edges and their attributes. - The index uses edge IDs, from 0 to M - 1 where M is the number of - edges. The first two columns of the dataframe represent the IDs of - source and target vertices for each edge. These columns have names - "source" and "target". If your edges have attributes with the same - names, they will be present in the dataframe, but not in the first - two columns. - """ - try: - import pandas as pd - except ImportError: - raise ImportError("You should install pandas in order to use this function") - - df = pd.DataFrame( - {attr: self.es[attr] for attr in self.edge_attributes()}, - index=list(range(self.ecount())), - ) - df.index.name = "edge ID" - - df.insert(0, "source", [e.source for e in self.es], allow_duplicates=True) - df.insert(1, "target", [e.target for e in self.es], allow_duplicates=True) - - return df - def bipartite_projection( self, types="type", multiplicity=True, probe1=-1, which="both" ): @@ -4189,7 +2582,6 @@ def __plot__(self, backend, context, *args, **kwds): drawer = kwds.pop( "drawer_factory", DrawerDirectory.resolve(self, backend)(context), - ) drawer.draw(self, *args, **kwds) @@ -5317,4 +3709,34 @@ def write(graph, filename, *args, **kwds): config = init_configuration() -del construct_graph_from_formula + +# Remove constructors from namespace +del ( + construct_graph_from_formula, + _construct_graph_from_graphmlz_file, + _construct_graph_from_dimacs_file, + _construct_graph_from_pickle_file, + _construct_graph_from_picklez_file, + _construct_graph_from_adjacency_file, + _construct_graph_from_file, + _construct_graph_from_dict_list, + _construct_graph_from_tuple_list, + _construct_graph_from_list_dict, + _construct_graph_from_dict_dict, + _construct_graph_from_adjacency, + _construct_graph_from_weighted_adjacency, + _construct_graph_from_dataframe, + _construct_random_geometric_graph, + _construct_bipartite_graph, + _construct_incidence_bipartite_graph, + _construct_full_bipartite_graph, + _construct_random_bipartite_graph, + _construct_graph_from_networkx, + _export_graph_to_networkx, + _construct_graph_from_graph_tool, + _export_graph_to_graph_tool, + _export_graph_to_list_dict, + _export_graph_to_dict_dict, + _export_graph_to_dict_list, + _export_graph_to_tuple_list, +) diff --git a/src/igraph/io/__init__.py b/src/igraph/io/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/igraph/io/adjacency.py b/src/igraph/io/adjacency.py new file mode 100644 index 000000000..0cb81688e --- /dev/null +++ b/src/igraph/io/adjacency.py @@ -0,0 +1,146 @@ +from igraph.sparse_matrix import ( + _graph_from_sparse_matrix, + _graph_from_weighted_sparse_matrix, +) + + +def _construct_graph_from_adjacency(cls, matrix, mode="directed", *args, **kwargs): + """Generates a graph from its adjacency matrix. + + @param matrix: the adjacency matrix. Possible types are: + - a list of lists + - a numpy 2D array or matrix (will be converted to list of lists) + - a scipy.sparse matrix (will be converted to a COO matrix, but not + to a dense matrix) + - a pandas.DataFrame (column/row names must match, and will be used + as vertex names). + @param mode: the mode to be used. Possible values are: + - C{"directed"} - the graph will be directed and a matrix + element gives the number of edges between two vertex. + - C{"undirected"} - alias to C{"max"} for convenience. + - C{"max"} - undirected graph will be created and the number of + edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} + - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} + - C{"upper"} - undirected graph with the upper right triangle of + the matrix (including the diagonal) + - C{"lower"} - undirected graph with the lower left triangle of + the matrix (including the diagonal) + """ + # Deferred import to avoid cycles + from igraph import Graph + + try: + import numpy as np + except ImportError: + np = None + + try: + from scipy import sparse + except ImportError: + sparse = None + + try: + import pandas as pd + except ImportError: + pd = None + + if (sparse is not None) and isinstance(matrix, sparse.spmatrix): + return _graph_from_sparse_matrix(cls, matrix, mode=mode) + + if (pd is not None) and isinstance(matrix, pd.DataFrame): + vertex_names = matrix.index.tolist() + matrix = matrix.values + else: + vertex_names = None + + if (np is not None) and isinstance(matrix, np.ndarray): + matrix = matrix.tolist() + + graph = super(Graph, cls).Adjacency(matrix, mode=mode) + + # Add vertex names if present + if vertex_names is not None: + graph.vs["name"] = vertex_names + + return graph + + +def _construct_graph_from_weighted_adjacency( + cls, matrix, mode="directed", attr="weight", loops=True +): + """Generates a graph from its weighted adjacency matrix. + + @param matrix: the adjacency matrix. Possible types are: + - a list of lists + - a numpy 2D array or matrix (will be converted to list of lists) + - a scipy.sparse matrix (will be converted to a COO matrix, but not + to a dense matrix) + @param mode: the mode to be used. Possible values are: + - C{"directed"} - the graph will be directed and a matrix + element gives the number of edges between two vertex. + - C{"undirected"} - alias to C{"max"} for convenience. + - C{"max"} - undirected graph will be created and the number of + edges between vertex M{i} and M{j} is M{max(A(i,j), A(j,i))} + - C{"min"} - like C{"max"}, but with M{min(A(i,j), A(j,i))} + - C{"plus"} - like C{"max"}, but with M{A(i,j) + A(j,i)} + - C{"upper"} - undirected graph with the upper right triangle of + the matrix (including the diagonal) + - C{"lower"} - undirected graph with the lower left triangle of + the matrix (including the diagonal) + + These values can also be given as strings without the C{ADJ} prefix. + @param attr: the name of the edge attribute that stores the edge + weights. + @param loops: whether to include loop edges. When C{False}, the diagonal + of the adjacency matrix will be ignored. + + """ + # Deferred import to avoid cycles + from igraph import Graph + + try: + import numpy as np + except ImportError: + np = None + + try: + from scipy import sparse + except ImportError: + sparse = None + + try: + import pandas as pd + except ImportError: + pd = None + + if (sparse is not None) and isinstance(matrix, sparse.spmatrix): + return _graph_from_weighted_sparse_matrix( + cls, + matrix, + mode=mode, + attr=attr, + loops=loops, + ) + + if (pd is not None) and isinstance(matrix, pd.DataFrame): + vertex_names = matrix.index.tolist() + matrix = matrix.values + else: + vertex_names = None + + if (np is not None) and isinstance(matrix, np.ndarray): + matrix = matrix.tolist() + + graph = super(Graph, cls).Weighted_Adjacency( + matrix, + mode=mode, + attr=attr, + loops=loops, + ) + + # Add vertex names if present + if vertex_names is not None: + graph.vs["name"] = vertex_names + + return graph diff --git a/src/igraph/io/bipartite.py b/src/igraph/io/bipartite.py new file mode 100644 index 000000000..aa359b8f1 --- /dev/null +++ b/src/igraph/io/bipartite.py @@ -0,0 +1,152 @@ +def _construct_incidence_bipartite_graph( + cls, + matrix, + directed=False, + mode="out", + multiple=False, + weighted=None, + *args, + **kwds +): + """Creates a bipartite graph from an incidence matrix. + + Example: + + >>> g = Graph.Incidence([[0, 1, 1], [1, 1, 0]]) + + @param matrix: the incidence matrix. + @param directed: whether to create a directed graph. + @param mode: defines the direction of edges in the graph. If + C{"out"}, then edges go from vertices of the first kind + (corresponding to rows of the matrix) to vertices of the + second kind (the columns of the matrix). If C{"in"}, the + opposite direction is used. C{"all"} creates mutual edges. + Ignored for undirected graphs. + @param multiple: defines what to do with non-zero entries in the + matrix. If C{False}, non-zero entries will create an edge no matter + what the value is. If C{True}, non-zero entries are rounded up to + the nearest integer and this will be the number of multiple edges + created. + @param weighted: defines whether to create a weighted graph from the + incidence matrix. If it is c{None} then an unweighted graph is created + and the multiple argument is used to determine the edges of the graph. + If it is a string then for every non-zero matrix entry, an edge is created + and the value of the entry is added as an edge attribute named by the + weighted argument. If it is C{True} then a weighted graph is created and + the name of the edge attribute will be ‘weight’. + + @raise ValueError: if the weighted and multiple are passed together. + + @return: the graph with a binary vertex attribute named C{"type"} that + stores the vertex classes. + """ + is_weighted = True if weighted or weighted == "" else False + if is_weighted and multiple: + raise ValueError("arguments weighted and multiple can not co-exist") + result, types = cls._Incidence(matrix, directed, mode, multiple, *args, **kwds) + result.vs["type"] = types + if is_weighted: + weight_attr = "weight" if weighted is True else weighted + _, rows, _ = result.get_incidence() + num_vertices_of_first_kind = len(rows) + for edge in result.es: + source, target = edge.tuple + if source in rows: + edge[weight_attr] = matrix[source][target - num_vertices_of_first_kind] + else: + edge[weight_attr] = matrix[target][source - num_vertices_of_first_kind] + return result + + +def _construct_bipartite_graph(cls, types, edges, directed=False, *args, **kwds): + """Creates a bipartite graph with the given vertex types and edges. + This is similar to the default constructor of the graph, the + only difference is that it checks whether all the edges go + between the two vertex classes and it assigns the type vector + to a C{type} attribute afterwards. + + Examples: + + >>> g = Graph.Bipartite([0, 1, 0, 1], [(0, 1), (2, 3), (0, 3)]) + >>> g.is_bipartite() + True + >>> g.vs["type"] + [False, True, False, True] + + @param types: the vertex types as a boolean list. Anything that + evaluates to C{False} will denote a vertex of the first kind, + anything that evaluates to C{True} will denote a vertex of the + second kind. + @param edges: the edges as a list of tuples. + @param directed: whether to create a directed graph. Bipartite + networks are usually undirected, so the default is C{False} + + @return: the graph with a binary vertex attribute named C{"type"} that + stores the vertex classes. + """ + result = cls._Bipartite(types, edges, directed, *args, **kwds) + result.vs["type"] = [bool(x) for x in types] + return result + + +def _construct_full_bipartite_graph( + cls, n1, n2, directed=False, mode="all", *args, **kwds +): + """Generates a full bipartite graph (directed or undirected, with or + without loops). + + >>> g = Graph.Full_Bipartite(2, 3) + >>> g.is_bipartite() + True + >>> g.vs["type"] + [False, False, True, True, True] + + @param n1: the number of vertices of the first kind. + @param n2: the number of vertices of the second kind. + @param directed: whether tp generate a directed graph. + @param mode: if C{"out"}, then all vertices of the first kind are + connected to the others; C{"in"} specifies the opposite direction, + C{"all"} creates mutual edges. Ignored for undirected graphs. + + @return: the graph with a binary vertex attribute named C{"type"} that + stores the vertex classes. + """ + result, types = cls._Full_Bipartite(n1, n2, directed, mode, *args, **kwds) + result.vs["type"] = types + return result + + +def _construct_random_bipartite_graph( + cls, n1, n2, p=None, m=None, directed=False, neimode="all", *args, **kwds +): + """Generates a random bipartite graph with the given number of vertices and + edges (if m is given), or with the given number of vertices and the given + connection probability (if p is given). + + If m is given but p is not, the generated graph will have n1 vertices of + type 1, n2 vertices of type 2 and m randomly selected edges between them. If + p is given but m is not, the generated graph will have n1 vertices of type 1 + and n2 vertices of type 2, and each edge will exist between them with + probability p. + + @param n1: the number of vertices of type 1. + @param n2: the number of vertices of type 2. + @param p: the probability of edges. If given, C{m} must be missing. + @param m: the number of edges. If given, C{p} must be missing. + @param directed: whether to generate a directed graph. + @param neimode: if the graph is directed, specifies how the edges will be + generated. If it is C{"all"}, edges will be generated in both directions + (from type 1 to type 2 and vice versa) independently. If it is C{"out"} + edges will always point from type 1 to type 2. If it is C{"in"}, edges + will always point from type 2 to type 1. This argument is ignored for + undirected graphs. + """ + if p is None: + p = -1 + if m is None: + m = -1 + result, types = cls._Random_Bipartite( + n1, n2, p, m, directed, neimode, *args, **kwds + ) + result.vs["type"] = types + return result diff --git a/src/igraph/io/files.py b/src/igraph/io/files.py new file mode 100644 index 000000000..96c2ea41e --- /dev/null +++ b/src/igraph/io/files.py @@ -0,0 +1,487 @@ +import gzip +from shutil import copyfileobj +from warnings import warn + +from igraph._igraph import GraphBase +from igraph.utils import ( + named_temporary_file, +) + + +def _identify_format(filename): + """_identify_format(filename) + + Tries to identify the format of the graph stored in the file with the + given filename. It identifies most file formats based on the extension + of the file (and not on syntactic evaluation). The only exception is + the adjacency matrix format and the edge list format: the first few + lines of the file are evaluated to decide between the two. + + @note: Internal function, should not be called directly. + + @param filename: the name of the file or a file object whose C{name} + attribute is set. + @return: the format of the file as a string. + """ + import os.path + + if hasattr(filename, "name") and hasattr(filename, "read"): + # It is most likely a file + try: + filename = filename.name + except Exception: + return None + + root, ext = os.path.splitext(filename) + ext = ext.lower() + + if ext == ".gz": + _, ext2 = os.path.splitext(root) + ext2 = ext2.lower() + if ext2 == ".pickle": + return "picklez" + elif ext2 == ".graphml": + return "graphmlz" + + if ext in [ + ".graphml", + ".graphmlz", + ".lgl", + ".ncol", + ".pajek", + ".gml", + ".dimacs", + ".edgelist", + ".edges", + ".edge", + ".net", + ".pickle", + ".picklez", + ".dot", + ".gw", + ".lgr", + ".dl", + ]: + return ext[1:] + + if ext == ".txt" or ext == ".dat": + # Most probably an adjacency matrix or an edge list + f = open(filename, "r") + line = f.readline() + if line is None: + return "edges" + parts = line.strip().split() + if len(parts) == 2: + line = f.readline() + if line is None: + return "edges" + parts = line.strip().split() + if len(parts) == 2: + line = f.readline() + if line is None: + # This is a 2x2 matrix, it can be a matrix or an edge + # list as well and we cannot decide + return None + else: + parts = line.strip().split() + if len(parts) == 0: + return None + return "edges" + else: + # Not a matrix + return None + else: + return "adjacency" + + +def _construct_graph_from_adjacency_file( + cls, f, sep=None, comment_char="#", attribute=None, *args, **kwds +): + """Constructs a graph based on an adjacency matrix from the given file. + + Additional positional and keyword arguments not mentioned here are + passed intact to L{Adjacency}. + + @param f: the name of the file to be read or a file object + @param sep: the string that separates the matrix elements in a row. + C{None} means an arbitrary sequence of whitespace characters. + @param comment_char: lines starting with this string are treated + as comments. + @param attribute: an edge attribute name where the edge weights are + stored in the case of a weighted adjacency matrix. If C{None}, + no weights are stored, values larger than 1 are considered as + edge multiplicities. + @return: the created graph""" + if isinstance(f, str): + f = open(f) + + matrix, ri = [], 0 + for line in f: + line = line.strip() + if len(line) == 0: + continue + if line.startswith(comment_char): + continue + row = [float(x) for x in line.split(sep)] + matrix.append(row) + ri += 1 + + f.close() + + if attribute is None: + graph = cls.Adjacency(matrix, *args, **kwds) + else: + kwds["attr"] = attribute + graph = cls.Weighted_Adjacency(matrix, *args, **kwds) + + return graph + + +def _construct_graph_from_dimacs_file(cls, f, directed=False): + """Reads a graph from a file conforming to the DIMACS minimum-cost flow + file format. + + For the exact definition of the format, see + U{http://lpsolve.sourceforge.net/5.5/DIMACS.htm}. + + Restrictions compared to the official description of the format are + as follows: + + - igraph's DIMACS reader requires only three fields in an arc + definition, describing the edge's source and target node and + its capacity. + - Source vertices are identified by 's' in the FLOW field, target + vertices are identified by 't'. + - Node indices start from 1. Only a single source and target node + is allowed. + + @param f: the name of the file or a Python file handle + @param directed: whether the generated graph should be directed. + @return: the generated graph. The indices of the source and target + vertices are attached as graph attributes C{source} and C{target}, + the edge capacities are stored in the C{capacity} edge attribute. + """ + # Deferred import to avoid cycles + from igraph import Graph + + graph, source, target, cap = super(Graph, cls).Read_DIMACS(f, directed) + graph.es["capacity"] = cap + graph["source"] = source + graph["target"] = target + return graph + + +def _construct_graph_from_graphmlz_file(cls, f, directed=True, index=0): + """Reads a graph from a zipped GraphML file. + + @param f: the name of the file + @param index: if the GraphML file contains multiple graphs, + specified the one that should be loaded. Graph indices + start from zero, so if you want to load the first graph, + specify 0 here. + @return: the loaded graph object""" + with named_temporary_file() as tmpfile: + with open(tmpfile, "wb") as outf: + copyfileobj(gzip.GzipFile(f, "rb"), outf) + return cls.Read_GraphML(tmpfile, directed=directed, index=index) + + +def _construct_graph_from_pickle_file(cls, fname=None): + """Reads a graph from Python pickled format + + @param fname: the name of the file, a stream to read from, or + a string containing the pickled data. + @return: the created graph object. + """ + import pickle as pickle + + if hasattr(fname, "read"): + # Probably a file or a file-like object + result = pickle.load(fname) + else: + try: + fp = open(fname, "rb") + except UnicodeDecodeError: + try: + # We are on Python 3.6 or above and we are passing a pickled + # stream that cannot be decoded as Unicode. Try unpickling + # directly. + result = pickle.loads(fname) + except TypeError: + raise IOError( + "Cannot load file. If fname is a file name, that " + "filename may be incorrect." + ) + except IOError: + try: + # No file with the given name, try unpickling directly. + result = pickle.loads(fname) + except TypeError: + raise IOError( + "Cannot load file. If fname is a file name, that " + "filename may be incorrect." + ) + else: + result = pickle.load(fp) + fp.close() + + if not isinstance(result, cls): + raise TypeError("unpickled object is not a %s" % cls.__name__) + + return result + + +def _construct_graph_from_picklez_file(cls, fname): + """Reads a graph from compressed Python pickled format, uncompressing + it on-the-fly. + + @param fname: the name of the file or a stream to read from. + @return: the created graph object. + """ + import pickle as pickle + + if hasattr(fname, "read"): + # Probably a file or a file-like object + if isinstance(fname, gzip.GzipFile): + result = pickle.load(fname) + else: + result = pickle.load(gzip.GzipFile(mode="rb", fileobj=fname)) + else: + result = pickle.load(gzip.open(fname, "rb")) + + if not isinstance(result, cls): + raise TypeError("unpickled object is not a %s" % cls.__name__) + + return result + + +def _construct_graph_from_file(cls, f, format=None, *args, **kwds): + """Unified reading function for graphs. + + This method tries to identify the format of the graph given in + the first parameter and calls the corresponding reader method. + + The remaining arguments are passed to the reader method without + any changes. + + @param f: the file containing the graph to be loaded + @param format: the format of the file (if known in advance). + C{None} means auto-detection. Possible values are: C{"ncol"} + (NCOL format), C{"lgl"} (LGL format), C{"graphdb"} (GraphDB + format), C{"graphml"}, C{"graphmlz"} (GraphML and gzipped + GraphML format), C{"gml"} (GML format), C{"net"}, C{"pajek"} + (Pajek format), C{"dimacs"} (DIMACS format), C{"edgelist"}, + C{"edges"} or C{"edge"} (edge list), C{"adjacency"} + (adjacency matrix), C{"dl"} (DL format used by UCINET), + C{"pickle"} (Python pickled format), + C{"picklez"} (gzipped Python pickled format) + @raises IOError: if the file format can't be identified and + none was given. + """ + if format is None: + format = _identify_format(f) + try: + reader = cls._format_mapping[format][0] + except (KeyError, IndexError): + raise IOError("unknown file format: %s" % str(format)) + if reader is None: + raise IOError("no reader method for file format: %s" % str(format)) + reader = getattr(cls, reader) + return reader(f, *args, **kwds) + + +def _write_graph_to_adjacency_file(graph, f, sep=" ", eol="\n", *args, **kwds): + """Writes the adjacency matrix of the graph to the given file + + All the remaining arguments not mentioned here are passed intact + to L{Graph.get_adjacency}. + + @param f: the name of the file to be written. + @param sep: the string that separates the matrix elements in a row + @param eol: the string that separates the rows of the matrix. Please + note that igraph is able to read back the written adjacency matrix + if and only if this is a single newline character + """ + if isinstance(f, str): + f = open(f, "w") + matrix = graph.get_adjacency(*args, **kwds) + for row in matrix: + f.write(sep.join(map(str, row))) + f.write(eol) + f.close() + + +def _write_graph_to_dimacs_file( + graph, f, source=None, target=None, capacity="capacity" +): + """Writes the graph in DIMACS format to the given file. + + @param f: the name of the file to be written or a Python file handle. + @param source: the source vertex ID. If C{None}, igraph will try to + infer it from the C{source} graph attribute. + @param target: the target vertex ID. If C{None}, igraph will try to + infer it from the C{target} graph attribute. + @param capacity: the capacities of the edges in a list or the name of + an edge attribute that holds the capacities. If there is no such + edge attribute, every edge will have a capacity of 1. + """ + if source is None: + try: + source = graph["source"] + except KeyError: + raise ValueError( + "source vertex must be provided in the 'source' graph " + "attribute or in the 'source' argument of write_dimacs()" + ) + + if target is None: + try: + target = graph["target"] + except KeyError: + raise ValueError( + "target vertex must be provided in the 'target' graph " + "attribute or in the 'target' argument of write_dimacs()" + ) + + if isinstance(capacity, str) and capacity not in graph.edge_attributes(): + warn("'%s' edge attribute does not exist" % capacity) + capacity = [1] * graph.ecount() + + return GraphBase.write_dimacs(graph, f, source, target, capacity) + + +def _write_graph_to_graphmlz_file(graph, f, compresslevel=9): + """Writes the graph to a zipped GraphML file. + + The library uses the gzip compression algorithm, so the resulting + file can be unzipped with regular gzip uncompression (like + C{gunzip} or C{zcat} from Unix command line) or the Python C{gzip} + module. + + Uses a temporary file to store intermediate GraphML data, so + make sure you have enough free space to store the unzipped + GraphML file as well. + + @param f: the name of the file to be written. + @param compresslevel: the level of compression. 1 is fastest and + produces the least compression, and 9 is slowest and produces + the most compression.""" + with named_temporary_file() as tmpfile: + graph.write_graphml(tmpfile) + outf = gzip.GzipFile(f, "wb", compresslevel) + copyfileobj(open(tmpfile, "rb"), outf) + outf.close() + + +def _write_graph_to_pickle_file(graph, fname=None, version=-1): + """Saves the graph in Python pickled format + + @param fname: the name of the file or a stream to save to. If + C{None}, saves the graph to a string and returns the string. + @param version: pickle protocol version to be used. If -1, uses + the highest protocol available + @return: C{None} if the graph was saved successfully to the + given file, or a string if C{fname} was C{None}. + """ + import pickle as pickle + + if fname is None: + return pickle.dumps(graph, version) + if not hasattr(fname, "write"): + file_was_opened = True + fname = open(fname, "wb") + else: + file_was_opened = False + result = pickle.dump(graph, fname, version) + if file_was_opened: + fname.close() + return result + + +def _write_graph_to_picklez_file(graph, fname=None, version=-1): + """Saves the graph in Python pickled format, compressed with + gzip. + + Saving in this format is a bit slower than saving in a Python pickle + without compression, but the final file takes up much less space on + the hard drive. + + @param fname: the name of the file or a stream to save to. + @param version: pickle protocol version to be used. If -1, uses + the highest protocol available + @return: C{None} if the graph was saved successfully to the + given file. + """ + import pickle as pickle + + file_was_opened = False + + if not hasattr(fname, "write"): + file_was_opened = True + fname = gzip.open(fname, "wb") + elif not isinstance(fname, gzip.GzipFile): + file_was_opened = True + fname = gzip.GzipFile(mode="wb", fileobj=fname) + + result = pickle.dump(graph, fname, version) + + if file_was_opened: + fname.close() + + return result + + +def _write_graph_to_file(graph, f, format=None, *args, **kwds): + """Unified writing function for graphs. + + This method tries to identify the format of the graph given in + the first parameter (based on extension) and calls the corresponding + writer method. + + The remaining arguments are passed to the writer method without + any changes. + + @param f: the file containing the graph to be saved + @param format: the format of the file (if one wants to override the + format determined from the filename extension, or the filename itself + is a stream). C{None} means auto-detection. Possible values are: + + - C{"adjacency"}: adjacency matrix format + + - C{"dimacs"}: DIMACS format + + - C{"dot"}, C{"graphviz"}: GraphViz DOT format + + - C{"edgelist"}, C{"edges"} or C{"edge"}: numeric edge list format + + - C{"gml"}: GML format + + - C{"graphml"} and C{"graphmlz"}: standard and gzipped GraphML + format + + - C{"gw"}, C{"leda"}, C{"lgr"}: LEDA native format + + - C{"lgl"}: LGL format + + - C{"ncol"}: NCOL format + + - C{"net"}, C{"pajek"}: Pajek format + + - C{"pickle"}, C{"picklez"}: standard and gzipped Python pickled + format + + - C{"svg"}: SVG format + + @raises IOError: if the file format can't be identified and + none was given. + """ + if format is None: + format = _identify_format(f) + try: + writer = graph._format_mapping[format][1] + except (KeyError, IndexError): + raise IOError("unknown file format: %s" % str(format)) + if writer is None: + raise IOError("no writer method for file format: %s" % str(format)) + writer = getattr(graph, writer) + return writer(f, *args, **kwds) diff --git a/src/igraph/io/images.py b/src/igraph/io/images.py new file mode 100644 index 000000000..934ab01c0 --- /dev/null +++ b/src/igraph/io/images.py @@ -0,0 +1,365 @@ +import math + +from igraph.drawing import BoundingBox + + +def _write_graph_to_svg( + graph, + fname, + layout="auto", + width=None, + height=None, + labels="label", + colors="color", + shapes="shape", + vertex_size=10, + edge_colors="color", + edge_stroke_widths="width", + font_size=16, + *args, + **kwds +): + """Saves the graph as an SVG (Scalable Vector Graphics) file + + The file will be Inkscape (http://inkscape.org) compatible. + In Inkscape, as nodes are rearranged, the edges auto-update. + + @param fname: the name of the file or a Python file handle + @param layout: the layout of the graph. Can be either an + explicitly specified layout (using a list of coordinate + pairs) or the name of a layout algorithm (which should + refer to a method in the L{Graph} object, but without + the C{layout_} prefix. + @param width: the preferred width in pixels (default: 400) + @param height: the preferred height in pixels (default: 400) + @param labels: the vertex labels. Either it is the name of + a vertex attribute to use, or a list explicitly specifying + the labels. It can also be C{None}. + @param colors: the vertex colors. Either it is the name of + a vertex attribute to use, or a list explicitly specifying + the colors. A color can be anything acceptable in an SVG + file. + @param shapes: the vertex shapes. Either it is the name of + a vertex attribute to use, or a list explicitly specifying + the shapes as integers. Shape 0 means hidden (nothing is drawn), + shape 1 is a circle, shape 2 is a rectangle and shape 3 is a + rectangle that automatically sizes to the inner text. + @param vertex_size: vertex size in pixels + @param edge_colors: the edge colors. Either it is the name + of an edge attribute to use, or a list explicitly specifying + the colors. A color can be anything acceptable in an SVG + file. + @param edge_stroke_widths: the stroke widths of the edges. Either + it is the name of an edge attribute to use, or a list explicitly + specifying the stroke widths. The stroke width can be anything + acceptable in an SVG file. + @param font_size: font size. If it is a string, it is written into + the SVG file as-is (so you can specify anything which is valid + as the value of the C{font-size} style). If it is a number, it + is interpreted as pixel size and converted to the proper attribute + value accordingly. + """ + if width is None and height is None: + width = 400 + height = 400 + elif width is None: + width = height + elif height is None: + height = width + + if width <= 0 or height <= 0: + raise ValueError("width and height must be positive") + + if isinstance(layout, str): + layout = graph.layout(layout, *args, **kwds) + + if isinstance(labels, str): + try: + labels = graph.vs.get_attribute_values(labels) + except KeyError: + labels = [x + 1 for x in range(graph.vcount())] + elif labels is None: + labels = [""] * graph.vcount() + + if isinstance(colors, str): + try: + colors = graph.vs.get_attribute_values(colors) + except KeyError: + colors = ["red"] * graph.vcount() + + if isinstance(shapes, str): + try: + shapes = graph.vs.get_attribute_values(shapes) + except KeyError: + shapes = [1] * graph.vcount() + + if isinstance(edge_colors, str): + try: + edge_colors = graph.es.get_attribute_values(edge_colors) + except KeyError: + edge_colors = ["black"] * graph.ecount() + + if isinstance(edge_stroke_widths, str): + try: + edge_stroke_widths = graph.es.get_attribute_values(edge_stroke_widths) + except KeyError: + edge_stroke_widths = [2] * graph.ecount() + + if not isinstance(font_size, str): + font_size = "%spx" % str(font_size) + else: + if ";" in font_size: + raise ValueError("font size can't contain a semicolon") + + vcount = graph.vcount() + labels.extend(str(i + 1) for i in range(len(labels), vcount)) + colors.extend(["red"] * (vcount - len(colors))) + + if isinstance(fname, str): + f = open(fname, "w") + our_file = True + else: + f = fname + our_file = False + + bbox = BoundingBox(layout.bounding_box()) + + sizes = [width - 2 * vertex_size, height - 2 * vertex_size] + w, h = bbox.width, bbox.height + + ratios = [] + if w == 0: + ratios.append(1.0) + else: + ratios.append(sizes[0] / w) + if h == 0: + ratios.append(1.0) + else: + ratios.append(sizes[1] / h) + + layout = [ + [ + (row[0] - bbox.left) * ratios[0] + vertex_size, + (row[1] - bbox.top) * ratios[1] + vertex_size, + ] + for row in layout + ] + + directed = graph.is_directed() + + print('', file=f) + print( + "", + file=f, + ) + print(file=f) + print( + ''.format(width, height), end=" ", file=f) + + edge_color_dict = {} + print('', file=f) + for e_col in set(edge_colors): + if e_col == "#000000": + marker_index = "" + else: + marker_index = str(len(edge_color_dict)) + # Print an arrow marker for each possible line color + # This is a copy of Inkscape's standard Arrow 2 marker + print("', file=f) + print(" ', file=f) + print("", file=f) + + edge_color_dict[e_col] = "Arrow2Lend{0}".format(marker_index) + print("", file=f) + print( + '', + file=f, + ) + + for eidx, edge in enumerate(graph.es): + vidxs = edge.tuple + x1 = layout[vidxs[0]][0] + y1 = layout[vidxs[0]][1] + x2 = layout[vidxs[1]][0] + y2 = layout[vidxs[1]][1] + angle = math.atan2(y2 - y1, x2 - x1) + x2 = x2 - vertex_size * math.cos(angle) + y2 = y2 - vertex_size * math.sin(angle) + + print("', file=f) + + print(" ", file=f) + print(file=f) + + print( + ' ', + file=f, + ) + print(" ", file=f) + + if any(x == 3 for x in shapes): + # Only import tkFont if we really need it. Unfortunately, this will + # flash up an unneccesary Tk window in some cases + import tkinter.font + import tkinter as tk + + # This allows us to dynamically size the width of the nodes. + # Unfortunately this works only with font sizes specified in pixels. + if font_size.endswith("px"): + font_size_in_pixels = int(font_size[:-2]) + else: + try: + font_size_in_pixels = int(font_size) + except Exception: + raise ValueError( + "font sizes must be specified in pixels " + "when any of the nodes has shape=3 (i.e. " + "node size determined by text size)" + ) + tk_window = tk.Tk() + font = tkinter.font.Font( + root=tk_window, font=("Sans", font_size_in_pixels, tkinter.font.NORMAL) + ) + else: + tk_window = None + + for vidx in range(graph.vcount()): + print( + ' '.format( + vidx, layout[vidx][0], layout[vidx][1] + ), + file=f, + ) + if shapes[vidx] == 1: + # Undocumented feature: can handle two colors but only for circles + c = str(colors[vidx]) + if " " in c: + c = c.split(" ") + vs = str(vertex_size) + print( + ' '.format( + vs, c[0] + ), + file=f, + ) + print( + ' '.format( + vs, c[1] + ), + file=f, + ) + print( + ' '.format(vs), + file=f, + ) + else: + print( + ' '.format( + str(vertex_size), str(colors[vidx]) + ), + file=f, + ) + elif shapes[vidx] == 2: + print( + ' '.format( + vertex_size, vertex_size * 2, vidx, colors[vidx] + ), + file=f, + ) + elif shapes[vidx] == 3: + (vertex_width, vertex_height) = ( + font.measure(str(labels[vidx])) + 2, + font.metrics("linespace") + 2, + ) + print( + ' ".format( + vertex_width / 2.0, + vertex_height / 2.0, + vertex_width, + vertex_height, + vidx, + colors[vidx], + ), + file=f, + ) + + print( + ' '.format(vertex_size / 2.0, vidx, font_size), + file=f, + ) + print( + '' + "{2}".format(vertex_size / 2.0, vidx, str(labels[vidx])), + file=f, + ) + print(" ", file=f) + + print("", file=f) + print(file=f) + print("", file=f) + + if our_file: + f.close() + if tk_window: + tk_window.destroy() diff --git a/src/igraph/io/libraries.py b/src/igraph/io/libraries.py new file mode 100644 index 000000000..43512acfa --- /dev/null +++ b/src/igraph/io/libraries.py @@ -0,0 +1,267 @@ +def _export_graph_to_networkx( + graph, create_using=None, vertex_attr_hashable="_nx_name" +): + """Converts the graph to networkx format. + + igraph has ordered vertices and edges, but networkx does not. To keep + track of the original order, the '_igraph_index' vertex property is + added to both vertices and edges. + + @param create_using: specifies which NetworkX graph class to use when + constructing the graph. C{None} means to let igraph infer the most + appropriate class based on whether the graph is directed and whether + it has multi-edges. + @param vertex_attr_hashable (str): vertex attribute used to name vertices + in the exported network. The default "_nx_name" ensures round trip + conversions to/from networkx are lossless. + """ + import networkx as nx + + # Graph: decide on directness and mutliplicity + if create_using is None: + if graph.has_multiple(): + cls = nx.MultiDiGraph if graph.is_directed() else nx.MultiGraph + else: + cls = nx.DiGraph if graph.is_directed() else nx.Graph + else: + cls = create_using + + # Graph attributes + kw = {x: graph[x] for x in graph.attributes()} + g = cls(**kw) + + multigraph = isinstance(g, (nx.MultiGraph, nx.MultiDiGraph)) + + # Nodes and node attributes + for i, v in enumerate(graph.vs): + vattr = v.attributes() + vattr["_igraph_index"] = i + + # use _nx_name if the attribute is present so we can achieve + # a lossless round-trip in terms of vertex names + if vertex_attr_hashable in vattr: + hashable = vattr.pop(vertex_attr_hashable) + else: + hashable = i + + # adding nodes one at a time is not slower in networkx + g.add_node(hashable, **vattr) + + # Edges and edge attributes + for i, edge in enumerate(graph.es): + eattr = edge.attributes() + eattr["_igraph_index"] = i + + if multigraph and "_nx_multiedge_key" in eattr: + eattr["key"] = eattr.pop["_nx_multiedge_key"] + + if vertex_attr_hashable in graph.vertex_attributes(): + hashable_source = graph.vs[vertex_attr_hashable][edge.source] + hashable_target = graph.vs[vertex_attr_hashable][edge.target] + else: + hashable_source = edge.source + hashable_target = edge.target + + # adding edges one at a time is not slower in networkx + g.add_edge(hashable_source, hashable_target, **eattr) + + return g + + +def _construct_graph_from_networkx(cls, g, vertex_attr_hashable="_nx_name"): + """Converts the graph from networkx + + Vertex names will be stored as a vertex_attr_hashable attribute (usually + "_nx_name", but see below). Because python-igraph stored vertices in an + ordered manner, vertices will get new ids from 0 up. In case of + multigraphs, each edge will have an "_nx_multiedge_key" attribute, to + distinguish edges that connect the same two vertices. + + @param g: networkx Graph or DiGraph + @param vertex_attr_hashable (str): attribute used to store the Python + hashable used by networkx to identify each vertex. The default value + '_nx_name' ensures lossless round trip conversions to/from networkx. An + alternative choice is 'name': in that case, using strings for vertex + names is recommended and, if the graph is re-exported to networkx, + Graph.to_networkx(vertex_attr_hashable="name") must be used to recover + the correct vertex nomenclature in the exported network. + + """ + import networkx as nx + + # Graph attributes + gattr = dict(g.graph) + + # Nodes + vnames = list(g.nodes) + vattr = {vertex_attr_hashable: vnames} + vcount = len(vnames) + + # Dictionary connecting networkx hashables with igraph indices + if len(g) and "_igraph_index" in g.nodes[0]: + # Collect _igraph_index and fill gaps + idx = [x["_igraph_index"] for v, x in g.nodes.data()] + idx.sort() + idx_dict = {x: i for i, x in enumerate(idx)} + + vd = {} + for v, datum in g.nodes.data(): + vd[v] = idx_dict[datum["_igraph_index"]] + else: + vd = {v: i for i, v in enumerate(vnames)} + + # NOTE: we do not need a special class for multigraphs, it is taken + # care for at the edge level rather than at the graph level. + graph = cls( + n=vcount, directed=g.is_directed(), graph_attrs=gattr, vertex_attrs=vattr + ) + + # Vertex attributes + for v, datum in g.nodes.data(): + for key, val in list(datum.items()): + # Get rid of _igraph_index (we used it already) + if key == "_igraph_index": + continue + graph.vs[vd[v]][key] = val + + # Edges and edge attributes + eattr_names = {name for (_, _, data) in g.edges.data() for name in data} + eattr = {name: [] for name in eattr_names} + edges = [] + # Multigraphs need a hidden attribute for multiedges + if isinstance(g, (nx.MultiGraph, nx.MultiDiGraph)): + eattr["_nx_multiedge_key"] = [] + for (u, v, edgekey, data) in g.edges.data(keys=True): + edges.append((vd[u], vd[v])) + for name in eattr_names: + eattr[name].append(data.get(name)) + eattr["_nx_multiedge_key"].append(edgekey) + + else: + for (u, v, data) in g.edges.data(): + edges.append((vd[u], vd[v])) + for name in eattr_names: + eattr[name].append(data.get(name)) + + # Sort edges if there is a trace of a previous igraph ordering + if "_igraph_index" in eattr: + # Poor man's argsort + sortd = [(i, x) for i, x in enumerate(eattr["_igraph_index"])] + sortd.sort(key=lambda x: x[1]) + idx = [i for i, x in sortd] + + # Get rid of the _igraph_index now + del eattr["_igraph_index"] + + # Sort edges + edges = [edges[i] for i in idx] + # Sort each attribute + eattr = {key: [val[i] for i in idx] for key, val in eattr.items()} + + graph.add_edges(edges, eattr) + + return graph + + +def _export_graph_to_graph_tool( + graph, graph_attributes=None, vertex_attributes=None, edge_attributes=None +): + """Converts the graph to graph-tool + + Data types: graph-tool only accepts specific data types. See the + following web page for a list: + + https://graph-tool.skewed.de/static/doc/quickstart.html + + Note: because of the restricted data types in graph-tool, vertex and + edge attributes require to be type-consistent across all vertices or + edges. If you set the property for only some vertices/edges, the other + will be tagged as None in python-igraph, so they can only be converted + to graph-tool with the type 'object' and any other conversion will + fail. + + @param graph_attributes: dictionary of graph attributes to transfer. + Keys are attributes from the graph, values are data types (see + below). C{None} means no graph attributes are transferred. + @param vertex_attributes: dictionary of vertex attributes to transfer. + Keys are attributes from the vertices, values are data types (see + below). C{None} means no vertex attributes are transferred. + @param edge_attributes: dictionary of edge attributes to transfer. + Keys are attributes from the edges, values are data types (see + below). C{None} means no vertex attributes are transferred. + """ + import graph_tool as gt + + # Graph + g = gt.Graph(directed=graph.is_directed()) + + # Nodes + vc = graph.vcount() + g.add_vertex(vc) + + # Graph attributes + if graph_attributes is not None: + for x, dtype in list(graph_attributes.items()): + # Strange syntax for setting internal properties + gprop = g.new_graph_property(str(dtype)) + g.graph_properties[x] = gprop + g.graph_properties[x] = graph[x] + + # Vertex attributes + if vertex_attributes is not None: + for x, dtype in list(vertex_attributes.items()): + # Create a new vertex property + g.vertex_properties[x] = g.new_vertex_property(str(dtype)) + # Fill the values from the igraph.Graph + for i in range(vc): + g.vertex_properties[x][g.vertex(i)] = graph.vs[i][x] + + # Edges and edge attributes + if edge_attributes is not None: + for x, dtype in list(edge_attributes.items()): + g.edge_properties[x] = g.new_edge_property(str(dtype)) + for edge in graph.es: + e = g.add_edge(edge.source, edge.target) + if edge_attributes is not None: + for x, dtype in list(edge_attributes.items()): + prop = edge.attributes().get(x, None) + g.edge_properties[x][e] = prop + + return g + + +def _construct_graph_from_graph_tool(cls, g): + """Converts the graph from graph-tool + + @param g: graph-tool Graph + """ + # Graph attributes + gattr = dict(g.graph_properties) + + # Nodes + vcount = g.num_vertices() + + # Graph + graph = cls(n=vcount, directed=g.is_directed(), graph_attrs=gattr) + + # Node attributes + for key, val in g.vertex_properties.items(): + prop = val.get_array() + for i in range(vcount): + graph.vs[i][key] = prop[i] + + # Edges and edge attributes + # NOTE: graph-tool is quite strongly typed, so each property is always + # defined for all edges, using default values for the type. E.g. for a + # string property/attribute the missing edges get an empty string. + edges = [] + eattr_names = list(g.edge_properties) + eattr = {name: [] for name in eattr_names} + for e in g.edges(): + edges.append((int(e.source()), int(e.target()))) + for name, attr_map in g.edge_properties.items(): + eattr[name].append(attr_map[e]) + + graph.add_edges(edges, eattr) + + return graph diff --git a/src/igraph/io/objects.py b/src/igraph/io/objects.py new file mode 100644 index 000000000..8300e1e03 --- /dev/null +++ b/src/igraph/io/objects.py @@ -0,0 +1,831 @@ +from collections import defaultdict +from itertools import repeat +from warnings import warn + +from igraph.datatypes import UniqueIdGenerator + + +def _construct_graph_from_dict_list( + cls, + vertices, + edges, + directed=False, + vertex_name_attr="name", + edge_foreign_keys=("source", "target"), + iterative=False, +): + """Constructs a graph from a list-of-dictionaries representation. + + This function is useful when you have two lists of dictionaries, one for + vertices and one for edges, each containing their attributes (e.g. name, + weight). Of course, the edge dictionary must also contain two special keys + that indicate the source and target vertices connected by that edge. + Non-list iterables should work as long as they yield dictionaries or + dict-like objects (they should have the 'items' and '__getitem__' methods). + For instance, a database query result is likely to be fit as long as it's + iterable and yields dict-like objects with every iteration. + + @param vertices: the list of dictionaries for the vertices or C{None} if + there are no special attributes assigned to vertices and we + should simply use the edge list of dicts to infer vertex names. + @param edges: the list of dictionaries for the edges. Each dict must have + at least the two keys specified by edge_foreign_keys to label the source + and target vertices, while additional items will be treated as edge + attributes. + @param directed: whether the constructed graph will be directed + @param vertex_name_attr: the name of the distinguished key in the + dicts in the vertex data source that contains the vertex names. + Ignored if C{vertices} is C{None}. + @param edge_foreign_keys: tuple specifying the attributes in each edge + dictionary that contain the source (1st) and target (2nd) vertex names. + These items of each dictionary are also added as edge_attributes. + @param iterative: whether to add the edges to the graph one by one, + iteratively, or to build a large edge list first and use that to + construct the graph. The latter approach is faster but it may + not be suitable if your dataset is large. The default is to + add the edges in a batch from an edge list. + @return: the graph that was constructed + + Example: + + >>> vertices = [{'name': 'apple'}, {'name': 'pear'}, {'name': 'peach'}] + >>> edges = [{'source': 'apple', 'target': 'pear', 'weight': 1.2}, + ... {'source': 'apple', 'target': 'peach', 'weight': 0.9}] + >>> g = Graph.DictList(vertices, edges) + + The graph has three vertices with names and two edges with weights. + """ + + def create_list_from_indices(indices, n): + result = [None] * n + for i, v in indices: + result[i] = v + return result + + # Construct the vertices + vertex_attrs = {} + n = 0 + if vertices: + for idx, vertex_data in enumerate(vertices): + for k, v in vertex_data.items(): + try: + vertex_attrs[k].append((idx, v)) + except KeyError: + vertex_attrs[k] = [(idx, v)] + n += 1 + for k, v in vertex_attrs.items(): + vertex_attrs[k] = create_list_from_indices(v, n) + else: + vertex_attrs[vertex_name_attr] = [] + + if vertex_name_attr not in vertex_attrs: + raise AttributeError( + f'{vertex_name_attr} is not a key of your vertex dictionaries', + ) + vertex_names = vertex_attrs[vertex_name_attr] + + # Check for duplicates in vertex_names + if len(vertex_names) != len(set(vertex_names)): + raise ValueError("vertex names are not unique") + # Create a reverse mapping from vertex names to indices + vertex_name_map = UniqueIdGenerator(initial=vertex_names) + + # Construct the edges + efk_src, efk_dest = edge_foreign_keys + if iterative: + g = cls(n, [], directed, {}, vertex_attrs) + for idx, edge_data in enumerate(edges): + src_name = edge_data[efk_src] + dst_name = edge_data[efk_dest] + v1 = vertex_name_map[src_name] + if v1 == n: + g.add_vertices(1) + g.vs[n][vertex_name_attr] = src_name + n += 1 + v2 = vertex_name_map[dst_name] + if v2 == n: + g.add_vertices(1) + g.vs[n][vertex_name_attr] = dst_name + n += 1 + g.add_edge(v1, v2) + for k, v in edge_data.items(): + g.es[idx][k] = v + + return g + else: + edge_list = [] + edge_attrs = {} + m = 0 + for idx, edge_data in enumerate(edges): + v1 = vertex_name_map[edge_data[efk_src]] + v2 = vertex_name_map[edge_data[efk_dest]] + + edge_list.append((v1, v2)) + for k, v in edge_data.items(): + try: + edge_attrs[k].append((idx, v)) + except KeyError: + edge_attrs[k] = [(idx, v)] + m += 1 + for k, v in edge_attrs.items(): + edge_attrs[k] = create_list_from_indices(v, m) + + # It may have happened that some vertices were added during + # the process + if len(vertex_name_map) > n: + diff = len(vertex_name_map) - n + more = [None] * diff + for k, v in vertex_attrs.items(): + v.extend(more) + vertex_attrs[vertex_name_attr] = list(vertex_name_map.values()) + n = len(vertex_name_map) + + # Create the graph + return cls(n, edge_list, directed, {}, vertex_attrs, edge_attrs) + + +def _construct_graph_from_tuple_list( + cls, + edges, + directed=False, + vertex_name_attr="name", + edge_attrs=None, + weights=False, +): + """Constructs a graph from a list-of-tuples representation. + + This representation assumes that the edges of the graph are encoded + in a list of tuples (or lists). Each item in the list must have at least + two elements, which specify the source and the target vertices of the edge. + The remaining elements (if any) specify the edge attributes of that edge, + where the names of the edge attributes originate from the C{edge_attrs} + list. The names of the vertices will be stored in the vertex attribute + given by C{vertex_name_attr}. + + The default parameters of this function are suitable for creating + unweighted graphs from lists where each item contains the source vertex + and the target vertex. If you have a weighted graph, you can use items + where the third item contains the weight of the edge by setting + C{edge_attrs} to C{"weight"} or C{["weight"]}. If you have even more + edge attributes, add them to the end of each item in the C{edges} + list and also specify the corresponding edge attribute names in + C{edge_attrs} as a list. + + @param edges: the data source for the edges. This must be a list + where each item is a tuple (or list) containing at least two + items: the name of the source and the target vertex. Note that + names will be assigned to the C{name} vertex attribute (or another + vertex attribute if C{vertex_name_attr} is specified), even if + all the vertex names in the list are in fact numbers. + @param directed: whether the constructed graph will be directed + @param vertex_name_attr: the name of the vertex attribute that will + contain the vertex names. + @param edge_attrs: the names of the edge attributes that are filled + with the extra items in the edge list (starting from index 2, since + the first two items are the source and target vertices). If C{None} + or an empty sequence, only the source and target vertices will be + extracted and additional tuple items will be ignored. If a string, it is + interpreted as a single edge attribute. + @param weights: alternative way to specify that the graph is + weighted. If you set C{weights} to C{true} and C{edge_attrs} is + not given, it will be assumed that C{edge_attrs} is C{["weight"]} + and igraph will parse the third element from each item into an + edge weight. If you set C{weights} to a string, it will be assumed + that C{edge_attrs} contains that string only, and igraph will + store the edge weights in that attribute. + @return: the graph that was constructed + """ + if edge_attrs is None: + if not weights: + edge_attrs = () + else: + if not isinstance(weights, str): + weights = "weight" + edge_attrs = [weights] + else: + if weights: + raise ValueError("`weights` must be False if `edge_attrs` is " "not None") + + if isinstance(edge_attrs, str): + edge_attrs = [edge_attrs] + + # Set up a vertex ID generator + idgen = UniqueIdGenerator() + + # Construct the edges and the edge attributes + edge_list = [] + edge_attributes = {} + for name in edge_attrs: + edge_attributes[name] = [] + + for item in edges: + edge_list.append((idgen[item[0]], idgen[item[1]])) + for index, name in enumerate(edge_attrs, 2): + try: + edge_attributes[name].append(item[index]) + except IndexError: + edge_attributes[name].append(None) + + # Set up the name vertex attribute + vertex_attributes = {} + vertex_attributes[vertex_name_attr] = list(idgen.values()) + n = len(idgen) + + # Construct the graph + return cls(n, edge_list, directed, {}, vertex_attributes, edge_attributes) + + +def _construct_graph_from_list_dict( + cls, + edges, + directed=False, + vertex_name_attr="name", +): + """Constructs a graph from a dict-of-lists representation. + + This function is used to construct a graph from a dictionary of + lists. Other, non-list sequences (e.g. tuples) and lazy iterators are + are accepted. For each key x, its corresponding value must be a sequence of + multiple values y: the edge (x,y) will be created in the graph. x and y + must be either one of: + + - two integers: the vertices with those ids will be connected + - two strings: the vertices with those names will be connected + + If names are used, the order of vertices is not guaranteed, and each + vertex will be given the vertex_name_attr attribute. + + @param edges: the dict of sequences describing the edges + @param directed (bool): whether to create a directed graph + @vertex_name_attr (str): vertex attribute that will store the names + + @returns: a Graph object + + Example: + + >>> mydict = {'apple': ['pear', 'peach'], 'pear': ['peach']} + >>> g = Graph.ListDict(mydict) + + # The graph has three vertices with names and three edges connecting + # each pair. + """ + first_item = next(iter(edges), 0) + + if not isinstance(first_item, (int, str)): + raise ValueError("Keys must be integers or strings") + + vertex_attributes = {} + if isinstance(first_item, str): + name_map = UniqueIdGenerator() + edge_list = [] + for source, sequence in edges.items(): + source_id = name_map[source] + edge_list.extend((source_id, name_map[target]) for target in sequence) + vertex_attributes[vertex_name_attr] = name_map.values() + n = len(name_map) + + else: + edge_list = [] + n = -1 + for source, sequence in edges.items(): + n = max(n, source, *sequence) + edge_list.extend(zip(repeat(source), sequence)) + n += 1 + + # Construct the graph + return cls(n, edge_list, directed, {}, vertex_attributes, {}) + + +def _construct_graph_from_dict_dict( + cls, + edges, + directed=False, + vertex_name_attr="name", +): + """Constructs a graph from a dict-of-dicts representation. + + Each key can be an integer or a string and represent a vertex. Each value + is a dict representing edges (outgoing if the graph is directed) from that + vertex. Each dict key is an integer/string for a target vertex, such that + an edge will be created between those two vertices. Integers are + interpreted as vertex_ids from 0 (as used in igraph), strings are + interpreted as vertex names, in which case vertices are given separate + numeric ids. Each value is a dictionary of edge attributes for that edge. + + @param edges: the dict of dict of dicts specifying the edges and their + attributes + @param directed (bool): whether to create a directed graph + @vertex_name_attr (str): vertex attribute that will store the names + + @returns: a Graph object + + Example: + + {'Alice': {'Bob': {'weight': 1.5}, 'David': {'weight': 2}}} + + creates a graph with three vertices (Alice, Bob, and David) and two edges: + + - Alice - Bob (with weight 1.5) + - Alice - David (with weight 2) + """ + first_item = next(iter(edges), 0) + + if not isinstance(first_item, (int, str)): + raise ValueError("Keys must be integers or strings") + + vertex_attributes = {} + edge_attribute_list = [] + if isinstance(first_item, str): + name_map = UniqueIdGenerator() + edge_list = [] + for source, target_dict in edges.items(): + source_id = name_map[source] + for target, edge_attrs in target_dict.items(): + edge_list.append((source_id, name_map[target])) + edge_attribute_list.append(edge_attrs) + vertex_attributes[vertex_name_attr] = name_map.values() + n = len(name_map) + + else: + edge_list = [] + n = -1 + for source, target_dict in edges.items(): + n = max(n, source, *target_dict) + for target, edge_attrs in target_dict.items(): + edge_list.append((source, target)) + edge_attribute_list.append(edge_attrs) + n += 1 + + # Construct graph without edge attributes + graph = cls(n, edge_list, directed, {}, vertex_attributes, {}) + + # Add edge attributes + for edge, edge_attrs in zip(graph.es, edge_attribute_list): + for key, val in edge_attrs.items(): + edge[key] = val + + return graph + + +def _construct_graph_from_dataframe( + cls, + edges, + directed=True, + vertices=None, + use_vids=True, +): + """Generates a graph from one or two dataframes. + + @param edges: pandas DataFrame containing edges and metadata. The first + two columns of this DataFrame contain the source and target vertices + for each edge. These indicate the vertex IDs as nonnegative integers + rather than vertex names unless `use_vids` is False. Further columns + may contain edge attributes. + @param directed: bool setting whether the graph is directed + @param vertices: None (default) or pandas DataFrame containing vertex + metadata. The DataFrame's index must contain the vertex IDs as a + sequence of intergers from `0` to `len(vertices) - 1`. If `use_vids` + is False, the first column must contain the unique vertex *names*. + Vertex names should be strings for full compatibility, but many functions + will work if you set the name with any hashable object. All other columns + will be added as vertex attributes by column name. + @param use_vids: whether to interpret the first two columns of the `edges` + argument as vertex ids (0-based integers) instead of vertex names. + If this argument is set to True and the first two columns of `edges` + are not integers, an error is thrown. + + @return: the graph + + Vertex names in either the `edges` or `vertices` arguments that are set + to NaN (not a number) will be set to the string "NA". That might lead + to unexpected behaviour: fill your NaNs with values before calling this + function to mitigate. + """ + # Deferred import to avoid cycles + from igraph import Graph + + try: + import pandas as pd + except ImportError: + raise ImportError("You should install pandas in order to use this function") + try: + import numpy as np + except: + raise ImportError("You should install numpy in order to use this function") + + if edges.shape[1] < 2: + raise ValueError("The 'edges' DataFrame must contain at least two columns") + if vertices is not None and vertices.shape[1] < 1: + raise ValueError("The 'vertices' DataFrame must contain at least one column") + + if use_vids: + if not ( + str(edges.dtypes[0]).startswith("int") + and str(edges.dtypes[1]).startswith("int") + ): + raise TypeError( + f"Source and target IDs must be 0-based integers, found types {edges.dtypes.tolist()[:2]}" + ) + elif (edges.iloc[:, :2] < 0).any(axis=None): + raise ValueError("Source and target IDs must not be negative") + if vertices is not None: + vertices = vertices.sort_index() + if not vertices.index.equals( + pd.RangeIndex.from_range(range(vertices.shape[0])) + ): + if not str(vertices.index.dtype).startswith("int"): + raise TypeError( + f"Vertex IDs must be 0-based integers, found type {vertices.index.dtype}" + ) + elif (vertices.index < 0).any(axis=None): + raise ValueError("Vertex IDs must not be negative") + else: + raise ValueError( + f"Vertex IDs must be an integer sequence from 0 to {vertices.shape[0] - 1}" + ) + else: + # Handle if some source and target names in 'edges' are 'NA' + if edges.iloc[:, :2].isna().any(axis=None): + warn( + "In the first two columns of 'edges' NA elements were replaced with string \"NA\"" + ) + edges = edges.copy() + edges.iloc[:, :2].fillna("NA", inplace=True) + + # Bring DataFrame(s) into same format as with 'use_vids=True' + if vertices is None: + vertices = pd.DataFrame({"name": np.unique(edges.values[:, :2])}) + + if vertices.iloc[:, 0].isna().any(): + warn( + "In the first column of 'vertices' NA elements were replaced with string \"NA\"" + ) + vertices = vertices.copy() + vertices.iloc[:, 0].fillna("NA", inplace=True) + + if vertices.iloc[:, 0].duplicated().any(): + raise ValueError("Vertex names must be unique") + + if vertices.shape[1] > 1 and "name" in vertices.columns[1:]: + raise ValueError( + "Vertex attribute conflict: DataFrame already contains column 'name'" + ) + + vertices = vertices.rename({vertices.columns[0]: "name"}, axis=1).reset_index( + drop=True + ) + + # Map source and target names in 'edges' to IDs + vid_map = pd.Series(vertices.index, index=vertices.iloc[:, 0]) + edges = edges.copy() + edges.iloc[:, 0] = edges.iloc[:, 0].map(vid_map) + edges.iloc[:, 1] = edges.iloc[:, 1].map(vid_map) + + # Create graph + if vertices is None: + nv = edges.iloc[:, :2].max().max() + 1 + g = cls(n=nv, directed=directed) + else: + if not edges.iloc[:, :2].isin(vertices.index).all(axis=None): + raise ValueError( + "Some vertices in the edge DataFrame are missing from vertices DataFrame" + ) + nv = vertices.shape[0] + g = cls(n=nv, directed=directed) + # Add vertex attributes + for col in vertices.columns: + g.vs[col] = vertices[col].tolist() + + # add edges including optional attributes + e_list = list(edges.iloc[:, :2].itertuples(index=False, name=None)) + e_attr = edges.iloc[:, 2:].to_dict(orient="list") if edges.shape[1] > 2 else None + g.add_edges(e_list, e_attr) + + return g + + +def _export_graph_to_dict_list( + graph, use_vids=True, skip_none=False, vertex_name_attr="name" +): + """Export graph as two lists of dictionaries, for vertices and edges. + + This function is the reverse of Graph.DictList. + + @param use_vids (bool): whether to label vertices in the output data + structure by their ids or their vertex_name_attr attribute. If + use_vids=False but vertices lack a vertex_name_attr attribute, an + AttributeError is raised. + @param skip_none (bool): whether to skip, for each edge, attributes that + have a value of None. This is useful if only some edges are expected to + possess an attribute. + @vertex_name_attr (str): only used with use_vids=False to choose what + vertex attribute to use to name your vertices in the output data + structure. + + @return: a tuple with two lists of dictionaries, representing the vertices + and the edges, respectively, with their attributes. + + Example: + + >>> g = Graph([(0, 1), (1, 2)]) + >>> g.vs["name"] = ["apple", "pear", "peach"] + >>> g.es["name"] = ["first_edge", "second"] + + >>> g.to_dict_list() + ([{"name": "apple"}, {"name": "pear"}, {"name": "peach"}], + [{"source": 0, "target": 1, "name": "first_edge"}, + {"source" 0, "target": 2, name": "second"}]) + + >>> g.to_dict_list(use_vids=False) + ([{"name": "apple"}, {"name": "pear"}, {"name": "peach"}], + [{"source": "apple", "target": "pear", "name": "first_edge"}, + {"source" "apple", "target": "peach", name": "second"}]) + """ + # Output data structures + res_vs, res_es = [], [] + + if not use_vids: + if vertex_name_attr not in graph.vertex_attributes(): + raise AttributeError(f"No vertex attribute {vertex_name_attr}") + + vs_names = graph.vs[vertex_name_attr] + + for vertex in graph.vs: + if skip_none: + attrdic = {k: v for k, v in vertex.attributes() if v is not None} + else: + attrdic = vertex.attributes() + res_vs.append(attrdic) + + for edge in graph.es: + source, target = edge.tuple + if not use_vids: + source, target = vs_names[source], vs_names[target] + if skip_none: + attrdic = {k: v for k, v in edge.attributes() if v is not None} + else: + attrdic = edge.attributes() + + attrdic["source"] = source + attrdic["target"] = target + res_es.append(attrdic) + + return (res_vs, res_es) + + +def _export_graph_to_tuple_list( + graph, use_vids=True, edge_attrs=None, vertex_name_attr="name" +): + """Export graph to a list of edge tuples + + This function is the reverse of Graph.TupleList. + + @param use_vids (bool): whether to label vertices in the output data + structure by their ids or their vertex_name_attr attribute. If + use_vids=False but vertices lack a vertex_name_attr attribute, an + AttributeError is raised. + @param edge_attrs (str or list of str): list of edge attributes to export + in addition to source and target vertex, which are always the first two + elements of each tuple. None (default) is equivalent to an empty list. A + string is acceptable to signify a single attribute and will be wrapped in + a list internally. + @vertex_name_attr (str): only used with use_vids=False to choose what + vertex attribute to use to name your vertices in the output data + structure. + + @return: a list of tuples, each representing an edge of the graph. + + Example: + + >>> g = Graph.Full(3) + >>> g.vs["name"] = ["apple", "pear", "peach"] + >>> g.es["name"] = ["first_edge", "second", "third"] + + # Get name of the edge + >>> g.to_tuple_list(edge_attrs=["name"]) + [(0, 1, "first_edge"), (0, 2, "second"), (1, 2, "third")] + + # Use vertex names, no edge attributes + >>> g.to_tuple_list(use_vids=False) + [("apple", "pear"), ("apple", "peach"), ("pear", "peach")] + """ + # Output data structure + res = [] + + if edge_attrs is not None: + if isinstance(edge_attrs, str): + edge_attrs = [edge_attrs] + missing_attrs = list(set(edge_attrs) - set(graph.edge_attributes())) + if missing_attrs: + raise AttributeError(f"Missing attributes: {missing_attrs}") + else: + edge_attrs = [] + + if use_vids is False: + if vertex_name_attr not in graph.vertex_attributes(): + raise AttributeError(f"No vertex attribute {vertex_name_attr}") + + vs_names = graph.vs[vertex_name_attr] + + for edge in graph.es: + source, target = edge.tuple + if not use_vids: + source, target = vs_names[source], vs_names[target] + attrlist = [source, target] + attrlist += [edge[attrname] for attrname in edge_attrs] + res.append(tuple(attrlist)) + + return res + + +def _export_graph_to_list_dict( + graph, use_vids=True, sequence_constructor=list, vertex_name_attr="name", +): + """Export graph to a dictionary of lists (or other sequences). + + This function is the reverse of Graph.ListDict. + + @param use_vids (bool): whether to label vertices in the output data + structure by their ids or their vertex_name_attr attribute. If + use_vids=False but vertices lack a vertex_name_attr attribute, an + AttributeError is raised. + @param sequence_constructor (function): constructor for the data structure + to be used as values of the dictionary. The default (list) makes a dict + of lists, with each list representing the neighbors of the vertex + specified in the respective dictionary key. + @vertex_name_attr (str): only used with use_vids=False to choose what + vertex attribute to use to name your vertices in the output data + structure. + + @return: dictionary of sequences, keyed by vertices, with each value + containing the neighbors of that vertex. + + Example: + + >>> g = Graph.Full(3) + >>> g.to_sequence_dict() -> {0: [1, 2], 1: [2]} + >>> g.to_sequence_dict(sequence_constructor=tuple) -> {0: (1, 2), 1: (2,)} + >>> g.vs['name'] = ['apple', 'pear', 'peach'] + >>> g.to_sequence_dict(use_vids=False) + {'apple': ['pear', 'peach'], 'pear': ['peach']} + """ + if not use_vids: + if vertex_name_attr not in graph.vertex_attributes(): + raise AttributeError(f'Vertices do not have a {vertex_name_attr} attribute') + vs_names = graph.vs[vertex_name_attr] + + # Temporary output data structure + res = defaultdict(list) + + for edge in graph.es: + source, target = edge.tuple + + if not use_vids: + source = vs_names[source] + target = vs_names[target] + + res[source].append(target) + + res = {key: sequence_constructor(val) for key, val in res.items()} + return res + + +def _export_graph_to_dict_dict(graph, use_vids=True, edge_attrs=None, skip_none=False, vertex_name_attr="name"): + """Export graph to dictionary of dicts of edge attributes + + This function is the reverse of Graph.DictDict. + + @param use_vids (bool): whether to label vertices in the output data + structure by their ids or their vertex_name_attr attribute. If + use_vids=False but vertices lack a vertex_name_attr attribute, an + AttributeError is raised. + @param edge_attrs (str or list of str): list of edge attributes to export. + None (default) signified all attributes (unlike Graph.to_tuple_list). A + string is acceptable to signify a single attribute and will be wrapped + in a list internally. + @param skip_none (bool): whether to skip, for each edge, attributes that + have a value of None. This is useful if only some edges are expected to + possess an attribute. + @vertex_name_attr (str): only used with use_vids=False to choose what + vertex attribute to use to name your vertices in the output data + structure. + + @return: dictionary of dictionaries of dictionaries, with the outer keys + vertex ids/names, the middle keys ids/names of their neighbors, and the + innermost dictionary representing attributes of that edge. + + Example: + >>> g = Graph.Full(3) + >>> g.es['name'] = ['first_edge', 'second', 'third'] + >>> g.to_dict_dict() + {0: {1: {'name': 'first_edge'}, 2: {'name': 'second'}}, + 1: {2: {'name': 'third'}}} + """ + if edge_attrs is not None: + if isinstance(edge_attrs, str): + edge_attrs = [edge_attrs] + missing_attrs = list(set(edge_attrs) - set(graph.edge_attributes())) + if missing_attrs: + raise AttributeError(f"Missing attributes: {missing_attrs}") + + if not use_vids: + if vertex_name_attr not in graph.vertex_attributes(): + raise AttributeError(f'Vertices do not have a {vertex_name_attr} attribute') + vs_names = graph.vs[vertex_name_attr] + + # Temporary output data structure + res = defaultdict(lambda: defaultdict(dict)) + + for edge in graph.es: + source, target = edge.tuple + + if not use_vids: + source = vs_names[source] + target = vs_names[target] + + attrdic = edge.attributes() + if edge_attrs is not None: + attrdic = {k: attrdic[k] for k in edge_attrs} + if skip_none: + attrdic = {k: v for k, v in attrdic.items() if v is not None} + + res[source][target] = attrdic + + res = {key: dict(val) for key, val in res.items()} + return res + + +def _export_vertex_dataframe(graph): + """Export vertices with attributes to pandas.DataFrame + + If you want to use vertex names as index, you can do: + + >>> from string import ascii_letters + >>> graph = Graph.GRG(25, 0.4) + >>> graph.vs["name"] = ascii_letters[:graph.vcount()] + >>> df = graph.get_vertex_dataframe() + >>> df.set_index('name', inplace=True) + + @return: a pandas.DataFrame representing vertices and their attributes. + The index uses vertex IDs, from 0 to N - 1 where N is the number of + vertices. + """ + try: + import pandas as pd + except ImportError: + raise ImportError("You should install pandas in order to use this function") + + df = pd.DataFrame( + {attr: graph.vs[attr] for attr in graph.vertex_attributes()}, + index=list(range(graph.vcount())), + ) + df.index.name = "vertex ID" + + return df + + +def _export_edge_dataframe(graph): + """Export edges with attributes to pandas.DataFrame + + If you want to use source and target vertex IDs as index, you can do: + + >>> from string import ascii_letters + >>> graph = Graph.GRG(25, 0.4) + >>> graph.vs["name"] = ascii_letters[:graph.vcount()] + >>> df = graph.get_edge_dataframe() + >>> df.set_index(['source', 'target'], inplace=True) + + The index will be a pandas.MultiIndex. You can use the `drop=False` + option to keep the `source` and `target` columns. + + If you want to use vertex names in the source and target columns: + + >>> df = graph.get_edge_dataframe() + >>> df_vert = graph.get_vertex_dataframe() + >>> df['source'].replace(df_vert['name'], inplace=True) + >>> df['target'].replace(df_vert['name'], inplace=True) + >>> df_vert.set_index('name', inplace=True) # Optional + + @return: a pandas.DataFrame representing edges and their attributes. + The index uses edge IDs, from 0 to M - 1 where M is the number of + edges. The first two columns of the dataframe represent the IDs of + source and target vertices for each edge. These columns have names + "source" and "target". If your edges have attributes with the same + names, they will be present in the dataframe, but not in the first + two columns. + """ + try: + import pandas as pd + except ImportError: + raise ImportError("You should install pandas in order to use this function") + + df = pd.DataFrame( + {attr: graph.es[attr] for attr in graph.edge_attributes()}, + index=list(range(graph.ecount())), + ) + df.index.name = "edge ID" + + df.insert(0, "source", [e.source for e in graph.es], allow_duplicates=True) + df.insert(1, "target", [e.target for e in graph.es], allow_duplicates=True) + + return df diff --git a/src/igraph/io/random.py b/src/igraph/io/random.py new file mode 100644 index 000000000..7ae8e416a --- /dev/null +++ b/src/igraph/io/random.py @@ -0,0 +1,17 @@ +def _construct_random_geometric_graph(cls, n, radius, torus=False): + """Generates a random geometric graph. + + The algorithm drops the vertices randomly on the 2D unit square and + connects them if they are closer to each other than the given radius. + The coordinates of the vertices are stored in the vertex attributes C{x} + and C{y}. + + @param n: The number of vertices in the graph + @param radius: The given radius + @param torus: This should be C{True} if we want to use a torus instead of a + square. + """ + result, xs, ys = cls._GRG(n, radius, torus) + result.vs["x"] = xs + result.vs["y"] = ys + return result diff --git a/tests/test_basic.py b/tests/test_basic.py index 340621966..a46df8458 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -613,6 +613,88 @@ def checkIfOK(self, g, name_attr, edge_attrs): self.assertTrue(g.edge_attributes() == []) +class GraphListDictTests(unittest.TestCase): + def setUp(self): + self.eids = { + 0: [1], + 2: [1, 0], + 3: [0, 1], + } + self.edges = { + "Alice": ["Bob"], + "Cecil": ["Bob", "Alice"], + "David": ["Alice", "Bob"], + } + + def testEmptyGraphListDict(self): + g = Graph.ListDict({}) + self.assertEqual(g.vcount(), 0) + + def testGraphFromListDict(self): + g = Graph.ListDict(self.eids) + self.checkIfOK(g, ()) + + def testGraphFromListDictWithNames(self): + g = Graph.ListDict(self.edges) + self.checkIfOK(g, "name") + + def checkIfOK(self, g, name_attr): + self.assertTrue(g.vcount() == 4 and g.ecount() == 5 and not g.is_directed()) + self.assertTrue(g.get_edgelist() == [(0, 1), (1, 2), (0, 2), (0, 3), (1, 3)]) + self.assertTrue(g.attributes() == []) + if name_attr: + self.assertTrue(g.vertex_attributes() == [name_attr]) + self.assertTrue(g.vs[name_attr] == ["Alice", "Bob", "Cecil", "David"]) + self.assertTrue(g.edge_attributes() == []) + + +class GraphDictDictTests(unittest.TestCase): + def setUp(self): + self.eids = { + 0: {1: {}}, + 2: {1: {}, 0: {}}, + 3: {0: {}, 1: {}}, + } + self.edges = { + "Alice": {"Bob": {}}, + "Cecil": {"Bob": {}, "Alice": {}}, + "David": {"Alice": {}, "Bob": {}}, + } + self.eids_with_props = { + 0: {1: {"weight": 5.6, "additional": 'abc'}}, + 2: {1: {"weight": 3.4}, 0: {"weight": 2}}, + 3: {0: {"weight": 1}, 1: {"weight": 5.6}}, + } + + def testEmptyGraphDictDict(self): + g = Graph.DictDict({}) + self.assertEqual(g.vcount(), 0) + + def testGraphFromDictDict(self): + g = Graph.DictDict(self.eids) + self.checkIfOK(g, ()) + + def testGraphFromDictDict(self): + g = Graph.DictDict(self.eids_with_props) + self.checkIfOK(g, (), edge_attrs=["additional", "weight"]) + + def testGraphFromDictDictWithNames(self): + g = Graph.DictDict(self.edges) + self.checkIfOK(g, "name") + + def checkIfOK(self, g, name_attr, edge_attrs=None): + self.assertTrue(g.vcount() == 4 and g.ecount() == 5 and not g.is_directed()) + self.assertTrue(g.get_edgelist() == [(0, 1), (1, 2), (0, 2), (0, 3), (1, 3)]) + self.assertTrue(g.attributes() == []) + if name_attr: + self.assertTrue(g.vertex_attributes() == [name_attr]) + self.assertTrue(g.vs[name_attr] == ["Alice", "Bob", "Cecil", "David"]) + if edge_attrs is None: + self.assertEqual(g.edge_attributes(), []) + else: + self.assertEqual(sorted(g.edge_attributes()), sorted(edge_attrs)) + + class DegreeSequenceTests(unittest.TestCase): def testIsDegreeSequence(self): # Catch and suppress warnings because is_degree_sequence() is now @@ -827,6 +909,8 @@ def suite(): datatype_suite = unittest.makeSuite(DatatypeTests) graph_dict_list_suite = unittest.makeSuite(GraphDictListTests) graph_tuple_list_suite = unittest.makeSuite(GraphTupleListTests) + graph_list_dict_suite = unittest.makeSuite(GraphListDictTests) + graph_dict_dict_suite = unittest.makeSuite(GraphDictDictTests) degree_sequence_suite = unittest.makeSuite(DegreeSequenceTests) inheritance_suite = unittest.makeSuite(InheritanceTests) refcount_suite = unittest.makeSuite(ReferenceCountTests) @@ -836,6 +920,8 @@ def suite(): datatype_suite, graph_dict_list_suite, graph_tuple_list_suite, + graph_list_dict_suite, + graph_dict_dict_suite, degree_sequence_suite, inheritance_suite, refcount_suite diff --git a/tests/test_foreign.py b/tests/test_foreign.py index e86051b27..7ad7e2fd4 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -287,8 +287,7 @@ def testAdjacency(self): self.assertEqual(g.vcount(), 6) self.assertEqual(g.ecount(), 12) self.assertTrue(g.is_directed()) - self.assertTrue( - g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]) + self.assertTrue(g.es["weight"] == [1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2]) g.write_adjacency(tmpfname) @@ -405,6 +404,148 @@ def testPickle(self): self.assertTrue(g.vcount() == 3 and g.ecount() == 1 and not g.is_directed()) g.write_pickle(tmpfname) + def testDictList(self): + g = Graph.Full(3) + + # Check with vertex ids + self.assertEqual( + g.to_dict_list(), + ( + [{}, {}, {}], + [ + {"source": 0, "target": 1}, + {"source": 0, "target": 2}, + {"source": 1, "target": 2}, + ], + ), + ) + + # Check failure for vertex names + self.assertRaises(AttributeError, g.to_dict_list, False) + + # Check with vertex names + g.vs["name"] = ["apple", "pear", "peach"] + self.assertEqual( + g.to_dict_list(), + ( + [{"name": "apple"}, {"name": "pear"}, {"name": "peach"}], + [ + {"source": 0, "target": 1}, + {"source": 0, "target": 2}, + {"source": 1, "target": 2}, + ], + ), + ) + self.assertEqual( + g.to_dict_list(use_vids=False), + ( + [{"name": "apple"}, {"name": "pear"}, {"name": "peach"}], + [ + {"source": "apple", "target": "pear"}, + {"source": "apple", "target": "peach"}, + {"source": "pear", "target": "peach"}, + ], + ), + ) + + def testTupleList(self): + g = Graph.Full(3) + + # Check with vertex ids + self.assertEqual( + g.to_tuple_list(), + [(0, 1), (0, 2), (1, 2)], + ) + + # Check failure for edge names + self.assertRaises(AttributeError, g.to_tuple_list, True, "name") + + # Edge attributes + g.es["name"] = ["first_edge", "second", None] + self.assertEqual( + g.to_tuple_list(edge_attrs="name"), + [(0, 1, "first_edge"), (0, 2, "second"), (1, 2, None)], + ) + self.assertEqual( + g.to_tuple_list(edge_attrs=["name"]), + [(0, 1, "first_edge"), (0, 2, "second"), (1, 2, None)], + ) + + # Missing vertex names + self.assertRaises(AttributeError, g.to_tuple_list, False) + + # Vertex names + g.vs["name"] = ["apple", "pear", "peach"] + self.assertEqual( + g.to_tuple_list(use_vids=False, edge_attrs="name"), + [ + ("apple", "pear", "first_edge"), + ("apple", "peach", "second"), + ("pear", "peach", None), + ], + ) + + def testSequenceDict(self): + g = Graph.Full(3) + + # Check with vertex ids + self.assertEqual(g.to_list_dict(), {0: [1, 2], 1: [2]}) + self.assertEqual( + g.to_list_dict(sequence_constructor=tuple), + {0: (1, 2), 1: (2,)}, + ) + + # Check failure for vertex names + self.assertRaises(AttributeError, g.to_list_dict, False) + + # Check with vertex names + g.vs["name"] = ["apple", "pear", "peach"] + self.assertEqual( + g.to_list_dict(use_vids=False), + {"apple": ["pear", "peach"], "pear": ["peach"]}, + ) + + def testDictDict(self): + g = Graph([(0, 1), (0, 2), (1, 2)]) + + # Check with vertex ids, no edge attrs + self.assertEqual( + g.to_dict_dict(), + {0: {1: {}, 2: {}}, 1: {2: {}}}, + ) + + # With vertex ids, edge attrs + g.es["name"] = ["first_edge", "second", None] + # Check with vertex ids, incomplete edge attrs + self.assertEqual( + g.to_dict_dict(), + { + 0: {1: {"name": "first_edge"}, 2: {"name": "second"}}, + 1: {2: {"name": None}}, + }, + ) + self.assertEqual( + g.to_dict_dict(skip_none=True), + {0: {1: {"name": "first_edge"}, 2: {"name": "second"}}, 1: {2: {}}}, + ) + + # With vertex names + g.vs["name"] = ["apple", "pear", "peach"] + self.assertEqual( + g.to_dict_dict(use_vids=False), + { + "apple": {"pear": {"name": "first_edge"}, "peach": {"name": "second"}}, + "pear": {"peach": {"name": None}}, + }, + ) + self.assertEqual( + g.to_dict_dict(use_vids=False, skip_none=True), + { + "apple": {"pear": {"name": "first_edge"}, "peach": {"name": "second"}}, + "pear": {"peach": {}}, + }, + ) + @unittest.skipIf(pd is None, "test case depends on Pandas") def testVertexDataFrames(self): g = Graph([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)]) @@ -483,7 +624,6 @@ def testEdgeDataFrames(self): i = 2 + list(df.columns[2:]).index("source") self.assertEqual(list(df.iloc[:, i]), g.es["source"]) - @unittest.skipIf(nx is None, "test case depends on networkx") def testGraphNetworkx(self): # Undirected @@ -549,8 +689,15 @@ def testMultigraphNetworkx(self): # Test attributes self.assertEqual(g.attributes(), g2.attributes()) - self.assertEqual(sorted(["vattr", "_nx_name"]), sorted(g2.vertex_attributes())) - self.assertEqual(g.edge_attributes(), g2.edge_attributes()) + self.assertEqual( + sorted(["vattr", "_nx_name"]), + sorted(g2.vertex_attributes()), + ) + self.assertEqual( + sorted(["eattr", "_nx_multiedge_key"]), + sorted(g2.edge_attributes()), + ) + # Testing parallel edges is a bit more tricky edge2_found = set() for edge in g.es: diff --git a/tests/test_generators.py b/tests/test_generators.py index 054b5313a..7a0a9a444 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -389,11 +389,12 @@ def testDataFrame(self): self.assertTrue(g.vs["name"] == [1, 2, 3, 4, 5, 6]) self.assertTrue(g.vs["label"] == ["1", "2", "3", "4", "5", "6"]) - # Vertex ids + # Vertex names edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) g = Graph.DataFrame(edges, use_vids=False) self.assertTrue(g.vcount() == 6) + # Vertex ids edges = pd.DataFrame({"source": [1, 2, 3], "target": [4, 5, 6]}) g = Graph.DataFrame(edges) self.assertTrue(g.vcount() == 7) From 0d9f989096129213650b26654dc73407ccef339a Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Sep 2021 17:41:17 +0200 Subject: [PATCH 0431/1681] ci: fix the path to arith_apple_m1.h --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 395e66f78..539cf065d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,7 +70,7 @@ jobs: - cmake_arch: x86_64 wheel_arch: x86_64 - cmake_arch: arm64 - cmake_extra_args: -DF2C_EXTERNAL_ARITH_HEADER=etc/arith_apple_m1.h + cmake_extra_args: -DF2C_EXTERNAL_ARITH_HEADER=../../../etc/arith_apple_m1.h wheel_arch: arm64 steps: From c272841ff6d1017d0fa2edb96f3454afe1b1a870 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Sep 2021 17:47:03 +0200 Subject: [PATCH 0432/1681] ci: added missing etc/arith_apple_m1.h --- etc/arith_apple_m1.h | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 etc/arith_apple_m1.h diff --git a/etc/arith_apple_m1.h b/etc/arith_apple_m1.h new file mode 100644 index 000000000..86d7e1a1b --- /dev/null +++ b/etc/arith_apple_m1.h @@ -0,0 +1,10 @@ +/* pre-generated arith.h for f2c when compiling on Apple M1 */ +#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 From d53a98d8319a7ebbd142719c5013ab9538d2aca2 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 25 Sep 2021 22:44:39 +0200 Subject: [PATCH 0433/1681] ci: build macOS x86_64 and arm64 wheels separately --- .github/workflows/build.yml | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf561c68e..663c7facb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ env: CIBW_TEST_SKIP: "cp310-*" jobs: - build_wheels: + build_wheel_linux: name: Build wheels on Linux (${{ matrix.wheel_arch }}) runs-on: ubuntu-20.04 strategy: @@ -36,8 +36,8 @@ jobs: with: path: ./wheelhouse/*.whl - build_aarch64_wheels: - name: Build wheels on Linux AArch64 + build_wheel_linux_aarch64: + name: Build wheels on Linux (aarch64) if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') runs-on: ubuntu-20.04 steps: @@ -62,8 +62,15 @@ jobs: path: ./wheelhouse/*.whl build_wheel_macos: - name: Build wheels on macOS + name: Build wheels on macOS (${{ matrix.wheel_arch }}) runs-on: macos-10.15 + strategy: + matrix: + include: + - cmake_arch: x86_64 + wheel_arch: x86_64 + - cmake_arch: arm64 + wheel_arch: arm64 steps: - uses: actions/checkout@v2 @@ -76,7 +83,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-${{ runner.os }}-${{ hashFiles('.gitmodules') }} + key: C-core-cache-v1-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python @@ -91,8 +98,9 @@ jobs: - name: Build wheels uses: joerick/cibuildwheel@v2.1.1 env: - CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_APPLE_SILICON_PROCESSOR=${{ matrix.cmake_arch }} - uses: actions/upload-artifact@v2 with: From f89de44f25be8638223c69ce0cd851979d774f28 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 26 Sep 2021 17:09:49 +0200 Subject: [PATCH 0434/1681] chore: more project URLs in setup.py --- setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.py b/setup.py index fc8945562..5d5138f0d 100644 --- a/setup.py +++ b/setup.py @@ -817,6 +817,12 @@ def use_educated_guess(self) -> None: license="GNU General Public License (GPL)", author="Tamas Nepusz", author_email="ntamas@gmail.com", + project_urls={ + "Bug Tracker": "https://github.com/igraph/python-igraph/issues", + "CI": "https://github.com/igraph/python-igraph/actions", + "Documentation": "https://igraph.org/python/doc", + "Source Code": "https://github.com/igraph/python-igraph", + }, ext_modules=[igraph_extension], package_dir={"igraph": "src/igraph"}, packages=["igraph", "igraph.app", "igraph.drawing", "igraph.remote"], From 4672cdc0947bcec89147500e1dab2b39836c1556 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Tue, 28 Sep 2021 17:31:52 +0200 Subject: [PATCH 0435/1681] ci: another attempt at compiling the C core for Apple Silicon --- .github/workflows/build.yml | 5 +++-- etc/arith_apple_m1.h | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 etc/arith_apple_m1.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 663c7facb..d3b6b97b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -70,6 +70,7 @@ jobs: - cmake_arch: x86_64 wheel_arch: x86_64 - cmake_arch: arm64 + cmake_extra_args: -DF2C_EXTERNAL_ARITH_HEADER=../../../etc/arith_apple_m1.h wheel_arch: arm64 steps: @@ -83,7 +84,7 @@ jobs: uses: actions/cache@v2 with: path: vendor/install - key: C-core-cache-v1-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} + key: C-core-cache-v2-${{ runner.os }}-${{ matrix.cmake_arch }}-${{ hashFiles('.git/modules/vendor/source/igraph/HEAD') }} - uses: actions/setup-python@v2 name: Install Python @@ -100,7 +101,7 @@ jobs: env: CIBW_ARCHS_MACOS: "${{ matrix.wheel_arch }}" CIBW_BEFORE_BUILD: "python setup.py build_c_core" - IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_APPLE_SILICON_PROCESSOR=${{ matrix.cmake_arch }} + IGRAPH_CMAKE_EXTRA_ARGS: -DCMAKE_OSX_ARCHITECTURES=${{ matrix.cmake_arch }} ${{ matrix.cmake_extra_args }} - uses: actions/upload-artifact@v2 with: diff --git a/etc/arith_apple_m1.h b/etc/arith_apple_m1.h new file mode 100644 index 000000000..cbdbd5fdf --- /dev/null +++ b/etc/arith_apple_m1.h @@ -0,0 +1,9 @@ +#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 From f517c62a1ebf76505f8ad0c6956adad7b0d58988 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Wed, 29 Sep 2021 12:41:20 +1000 Subject: [PATCH 0436/1681] Update changelog --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81ff00f20..b03f5450c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,25 @@ ## [Unreleased] + +### Added + +- More robust support for Matplotlib and initial support for plotly as graph + plotting backends, controlled by a configuration option. See PR + [#425](https://github.com/igraph/python-igraph/pull/425) for more details. + +- Added support for additional ways to construct a graph, such as from a + dictionary of dictionaries, and to export a graph object back to those + data structures. See PR [#434](https://github.com/igraph/python-igraph/pull/434) + for more details. + +- Added support for graph chordality which was already available in the C core: + `Graph.is_chordal()`, `Graph.chordal_completion()`, and + `Graph.maximal_cardinality_search()`. See PR + [#437](https://github.com/igraph/python-igraph/pull/437) for more details. + Thanks to [@cptwunderlich](https://github.com/cptwunderlich) for requesting + this. + ### Changed - Improved performance of `Graph.DataFrame()`, thanks to From 3ef065005f20621525eea739d3ec6f842c54358d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 1 Oct 2021 17:44:05 +0200 Subject: [PATCH 0437/1681] chore: updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 312aeb0b0..78aa7fa13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ [@fwitter](https://github.com/user/fwitter). See PR [#418](https://github.com/igraph/python-igraph/pull/418) for more details. +### Fixed + +* Fixed the Apple Silicon wheels so they should now work out of the box on + newer Macs with Apple M1 CPUs. + ## [0.9.6] ### Fixed From 97be0c1762ef96feacc2180bd39a3d357bd7e1d2 Mon Sep 17 00:00:00 2001 From: John Boy Date: Fri, 1 Oct 2021 18:17:14 +0200 Subject: [PATCH 0438/1681] fix: handle text wrapping if width=0 (#439) Co-authored-by: John D. Boy --- src/igraph/drawing/text.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index eb248e5e7..2895d9061 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -107,11 +107,11 @@ def get_text_layout(self, x=None, y=None, width=None, wrap=False): line_height = ctx.font_extents()[2] - if wrap: - if width and width > 0: - iterlines = self._iterlines_wrapped(width) - else: - warn("ignoring wrap=True as no width was specified") + if wrap and width and width > 0: + iterlines = self._iterlines_wrapped(width) + elif wrap: + warn("ignoring wrap=True as no width was specified") + iterlines = self._iterlines() else: iterlines = self._iterlines() From d51b33fc790810d03615c02f5bce3ec2d1c85ebf Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 1 Oct 2021 18:19:32 +0200 Subject: [PATCH 0439/1681] chore: updated changelog [ci skip] --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78aa7fa13..dfd77b13a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,11 @@ * Fixed the Apple Silicon wheels so they should now work out of the box on newer Macs with Apple M1 CPUs. +* Fixed a bug that resulted in an unexpected error when plotting a graph with + `wrap_labels=True` if the size of one of the vertices was zero or negative, + thanks to [@jboynyc](https://github.com/user/jboynyc). See PR + [#439](https://github.com/igraph/python-igraph/pull/439) for more details. + ## [0.9.6] ### Fixed From 21b4d8013f545729e29925a621323e558e1fd1c3 Mon Sep 17 00:00:00 2001 From: "John D. Boy" Date: Mon, 4 Oct 2021 11:26:55 +0200 Subject: [PATCH 0440/1681] fix: recognize SVG format from file extension The list in Graph._identify_format omitted the .svg extension. I added it and alphabetized the list to make it easier to spot other omissions. --- src/igraph/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 530cbc3cb..e6194a9d2 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2842,23 +2842,24 @@ def _identify_format(cls, filename): return "graphmlz" if ext in [ + ".dimacs", + ".dl", + ".dot", + ".edge", + ".edgelist", + ".edges", + ".gml", ".graphml", ".graphmlz", + ".gw", ".lgl", + ".lgr", ".ncol", - ".pajek", - ".gml", - ".dimacs", - ".edgelist", - ".edges", - ".edge", ".net", + ".pajek", ".pickle", ".picklez", - ".dot", - ".gw", - ".lgr", - ".dl", + ".svg", ]: return ext[1:] From 2ea15a612cf0b03db9a4aa0d6f00f8746c2041eb Mon Sep 17 00:00:00 2001 From: "John D. Boy" Date: Mon, 4 Oct 2021 10:59:42 +0200 Subject: [PATCH 0441/1681] fix: handle PathLike in Graph.write and Graph.Read os.PathLike was introduced in Python 3.6, and given igraph's aim of supporting the three most recent minor versions of Python 3, depending on it should not be an issue. This is a minimal fix, which simply turns PathLike objects into their string representation and otherwise keeps existing behavior. A more ambitious fix would be to rewrite how files and paths are handled using pathlib.Path. --- src/igraph/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index e6194a9d2..1e9178902 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2916,6 +2916,8 @@ def Read(cls, f, format=None, *args, **kwds): @raises IOError: if the file format can't be identified and none was given. """ + if isinstance(f, os.PathLike): + f = str(f) if format is None: format = cls._identify_format(f) try: @@ -2973,6 +2975,8 @@ def write(self, f, format=None, *args, **kwds): @raises IOError: if the file format can't be identified and none was given. """ + if isinstance(f, os.PathLike): + f = str(f) if format is None: format = self._identify_format(f) try: From 800f619c1ee6b8e9a41a47550016c7da14457fc4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 4 Oct 2021 13:27:42 +0200 Subject: [PATCH 0442/1681] chore: fixing up changelog format to be consistent with the develop branch [ci skip] --- CHANGELOG.md | 96 ++++++++++++++++++++++++++-------------------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd77b13a..31a504c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,25 +4,25 @@ ### Added -* Added `Graph.is_chordal()` to test whether a graph is chordal and - `Graph.chordal_completion()` to return a possible (not necessary minimal) - chordal completion of a graph. - -* Added `Graph.maximum_cardinality_search()`, primarily as an aid for - `Graph.is_chordal()`. +- Added support for graph chordality which was already available in the C core: + `Graph.is_chordal()`, `Graph.chordal_completion()`, and + `Graph.maximal_cardinality_search()`. See PR + [#437](https://github.com/igraph/python-igraph/pull/437) for more details. + Thanks to [@cptwunderlich](https://github.com/cptwunderlich) for requesting + this. ### Changed -* Improved performance of `Graph.DataFrame()`, thanks to +- Improved performance of `Graph.DataFrame()`, thanks to [@fwitter](https://github.com/user/fwitter). See PR [#418](https://github.com/igraph/python-igraph/pull/418) for more details. ### Fixed -* Fixed the Apple Silicon wheels so they should now work out of the box on +- Fixed the Apple Silicon wheels so they should now work out of the box on newer Macs with Apple M1 CPUs. -* Fixed a bug that resulted in an unexpected error when plotting a graph with +- Fixed a bug that resulted in an unexpected error when plotting a graph with `wrap_labels=True` if the size of one of the vertices was zero or negative, thanks to [@jboynyc](https://github.com/user/jboynyc). See PR [#439](https://github.com/igraph/python-igraph/pull/439) for more details. @@ -31,7 +31,7 @@ ### Fixed -* Version 0.9.5 accidentally broke the Matplotlib backend when it was invoked +- Version 0.9.5 accidentally broke the Matplotlib backend when it was invoked without the `mark_groups=...` keyword argument; this version fixes the issue. Thanks to @dschult for reporting it! @@ -39,13 +39,13 @@ ### Fixed -* `plot(g, ..., mark_groups=True)` now works with the Matplotlib plotting backend. +- `plot(g, ..., mark_groups=True)` now works with the Matplotlib plotting backend. -* `set_random_number_generator(None)` now correctly switches back to igraph's +- `set_random_number_generator(None)` now correctly switches back to igraph's own random number generator instead of the default one that hooks into the `random` module of Python. -* Improved performance in cases when igraph has to call back to Python's +- Improved performance in cases when igraph has to call back to Python's `random` module to generate random numbers. One example is `Graph.Degree_Sequence(method="vl")`, whose performance suffered a more than 30x slowdown on 32-bit platforms before, compared to the native C @@ -58,73 +58,73 @@ ### Added -* Added `Graph.is_tree()` to test whether a graph is a tree. +- Added `Graph.is_tree()` to test whether a graph is a tree. -* Added `Graph.Realize_Degree_Sequence()` to construct a graph that realizes a +- Added `Graph.Realize_Degree_Sequence()` to construct a graph that realizes a given degree sequence, using a deterministic (Havel-Hakimi-style) algorithm. -* Added `Graph.Tree_Game()` to generate random trees with uniform sampling. +- Added `Graph.Tree_Game()` to generate random trees with uniform sampling. -* `Graph.to_directed()` now supports a `mode=...` keyword argument. +- `Graph.to_directed()` now supports a `mode=...` keyword argument. -* Added a `create_using=...` keyword argument to `Graph.to_networkx()` to +- Added a `create_using=...` keyword argument to `Graph.to_networkx()` to let the user specify which NetworkX class to use when converting the graph. ### Changed -* Updated igraph dependency to 0.9.4. +- Updated igraph dependency to 0.9.4. ### Fixed -* Improved performance of `Graph.from_networkx()` and `Graph.from_graph_tool()` +- Improved performance of `Graph.from_networkx()` and `Graph.from_graph_tool()` on large graphs, thanks to @szhorvat and @iosonofabio for fixing the issue. -* Fixed the `autocurve=...` keyword argument of `plot()` when using the +- Fixed the `autocurve=...` keyword argument of `plot()` when using the Matplotlib backend. ### Deprecated -* Functions and methods that take string arguments that represent an underlying +- Functions and methods that take string arguments that represent an underlying enum in the C core of igraph now print a deprecation warning when provided with a string that does not match one of the enum member names (as documented in the docstrings) exactly. Partial matches will be removed in the next minor or major version, whichever comes first. -* `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. +- `Graph.to_directed(mutual=...)` is now deprecated, use `mode=...` instead. -* `igraph.graph.drawing.UbiGraphDrawer` is deprecated as the upstream project +- `igraph.graph.drawing.UbiGraphDrawer` is deprecated as the upstream project is not maintained since 2008. ## [0.9.1] ### Changed -* Calling `plot()` without a filename or a target surface is now deprecated. +- Calling `plot()` without a filename or a target surface is now deprecated. The original intention was to plot to a temporary file and then open it in the default image viewer of the platform of the user automatically, but this has never worked reliably. The feature will be removed in 0.10.0. ### Fixed -* Fixed plotting of `VertexClustering` objects on Matplotlib axes. +- Fixed plotting of `VertexClustering` objects on Matplotlib axes. -* The `IGRAPH_CMAKE_EXTRA_ARGS` environment variable is now applied _after_ the +- The `IGRAPH_CMAKE_EXTRA_ARGS` environment variable is now applied _after_ the default CMake arguments when building the C core of igraph from source. This enables package maintainers to override any of the default arguments we pass to CMake. -* Fixed the documentation build by replacing Epydoc with PyDoctor. +- Fixed the documentation build by replacing Epydoc with PyDoctor. ### Miscellaneous -* Building `python-igraph` from source should not require `flex` and `bison` +- Building `python-igraph` from source should not require `flex` and `bison` any more; sources of the parsers used by the C core are now included in the Python source tarball. -* Many old code constructs that were used to maintain compatibility with Python +- Many old code constructs that were used to maintain compatibility with Python 2.x are removed now that we have dropped support for Python 2.x. -* Reading GraphML files is now also supported on Windows if you use one of the +- Reading GraphML files is now also supported on Windows if you use one of the official Python wheels. @@ -132,27 +132,27 @@ ### Added -* `Graph.DataFrame` now has a `use_vids=...` keyword argument that decides whether +- `Graph.DataFrame` now has a `use_vids=...` keyword argument that decides whether the data frame contains vertex IDs (`True`) or vertex names (`False`). (PR #348) -* Added `MatplotlibGraphDrawer` to draw a graph on an existing Matplotlib +- Added `MatplotlibGraphDrawer` to draw a graph on an existing Matplotlib figure. (PR #341) -* Added a code path to choose between preferred image viewers on FreeBSD. (PR #354) +- Added a code path to choose between preferred image viewers on FreeBSD. (PR #354) -* Added `Graph.harmonic_centrality()` that wraps `igraph_harmonic_centrality()` +- Added `Graph.harmonic_centrality()` that wraps `igraph_harmonic_centrality()` from the underlying C library. ### Changed -* `python-igraph` is now compatible with `igraph` 0.9.0. +- `python-igraph` is now compatible with `igraph` 0.9.0. -* The setup script was adapted to the new CMake-based build system of `igraph`. +- The setup script was adapted to the new CMake-based build system of `igraph`. -* Dropped support for older Python versions; the oldest Python version that +- Dropped support for older Python versions; the oldest Python version that `python-igraph` is tested on is now Python 3.6. -* The default splitting heuristic of the BLISS isomorphism algorithm was changed +- The default splitting heuristic of the BLISS isomorphism algorithm was changed from `IGRAPH_BLISS_FM` (first maximally non-trivially connected non-singleton cell) to `IGRAPH_BLISS_FL` (first largest non-singleton cell) as this seems to provide better performance on a variety of graph classes. This change is a follow-up @@ -160,28 +160,28 @@ ### Fixed -* Fixed crashes in the Python-C glue code related to the handling of empty +- Fixed crashes in the Python-C glue code related to the handling of empty vectors in certain attribute merging functions (see issue #358). -* Fixed a memory leak in `Graph.closeness_centrality()` when an invalid `cutoff` +- Fixed a memory leak in `Graph.closeness_centrality()` when an invalid `cutoff` argument was provided to the function. -* Clarified that the `fixed=...` argument is ineffective for the DrL layout +- Clarified that the `fixed=...` argument is ineffective for the DrL layout because the underlying C code does not handle it. The argument was _not_ removed for sake of backwards compatibility. -* `VertexSeq.find(name=x)` now works correctly when `x` is an integer; fixes +- `VertexSeq.find(name=x)` now works correctly when `x` is an integer; fixes #367 ### Miscellaneous -* The Python codebase was piped through `black` for consistent formatting. +- The Python codebase was piped through `black` for consistent formatting. -* Wildcard imports were removed from the codebase. +- Wildcard imports were removed from the codebase. -* CI tests were moved to Github Actions from Travis. +- CI tests were moved to Github Actions from Travis. -* The core C library is now built with `-fPIC` on Linux to allow linking to the +- The core C library is now built with `-fPIC` on Linux to allow linking to the Python interface. @@ -192,7 +192,7 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[Unreleased]: https://github.com/igraph/python-igraph/compare/0.9.6..HEAD +[unreleased]: https://github.com/igraph/python-igraph/compare/0.9.6..master [0.9.6]: https://github.com/igraph/python-igraph/compare/0.9.5...0.9.6 [0.9.5]: https://github.com/igraph/python-igraph/compare/0.9.4...0.9.5 [0.9.4]: https://github.com/igraph/python-igraph/compare/0.9.1...0.9.4 From f224e9995d4e23329b58a8e61ce3423d317f9af1 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 9 Oct 2021 20:49:16 +0200 Subject: [PATCH 0443/1681] doc: fix a few broken links --- src/_igraph/graphobject.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index ca0409bf9..39fcf0311 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -12759,7 +12759,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "the Chvatal graph, the Petersen graph or the Tutte graph. This method\n" "generates one of them based on its name (case insensitive). See the\n" "documentation of the C interface of C{igraph} for the names available:\n" - "U{http://igraph.org/doc/c}.\n\n" + "U{https://igraph.org/c/doc}.\n\n" "@param name: the name of the graph to be generated.\n" }, @@ -13113,7 +13113,7 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { " graphs are to be generated and execution time is not a concern.\n" " igraph uses the original implementation of Fabien Viger; see the\n" " following URL and the paper cited on it for the details of the\n" - " algorithm: U{http://www-rp.lip6.fr/~latapy/FV/generation.html}.\n" + " algorithm: U{https://www-complexnetworks.lip6.fr/~latapy/FV/generation.html}.\n" }, /* interface to igraph_isoclass_create */ @@ -15192,8 +15192,8 @@ struct PyMethodDef igraphmodule_Graph_methods[] = { "It is also useful for creating graphs from \"named\" (and\n" "optionally weighted) edge lists.\n\n" "This format is used by the Large Graph Layout program. See the\n" - "U{documentation of LGL }\n" - "regarding the exact format description.\n\n" + "U{repository of LGL }\n" + "for more information.\n\n" "LGL originally cannot deal with graphs containing multiple or loop\n" "edges, but this condition is not checked here, as igraph is happy\n" "with these.\n\n" From d0264f660a696de43869ece533f4acd42ad69123 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 9 Oct 2021 20:53:18 +0200 Subject: [PATCH 0444/1681] chore: updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31a504c50..f2a1472cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Thanks to [@cptwunderlich](https://github.com/cptwunderlich) for requesting this. +- `Graph.write()` and `Graph.Read()` now accept `Path` objects as well as + strings. See PR [#441](https://github.com/igraph/python-igraph/pull/441) for + more details. Thanks to [@jboynyc](https://github.com/jboynyc) for the + implementation. + ### Changed - Improved performance of `Graph.DataFrame()`, thanks to From 51e16d29c53c7bbfe4d757d2cec57ef6b9756b30 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sat, 9 Oct 2021 20:53:35 +0200 Subject: [PATCH 0445/1681] ci: NumPy and Pandas now have wheels for Python 3.10; still waiting for SciPy --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d3b6b97b0..ebd7e5fad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: [push, pull_request] env: CIBW_TEST_COMMAND: "cd {project} && python -m pytest tests" CIBW_TEST_EXTRAS: "test" - # skip testing on Python 3.10 until NumPy/SciPy/Pandas publish wheels + # skip testing on Python 3.10 until SciPy publishes wheels CIBW_TEST_SKIP: "cp310-*" jobs: From 38cdc604a80a82a57244b4d214cad2795a53dacc Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 13:53:22 +0200 Subject: [PATCH 0446/1681] test: added regression test for #446 --- tests/test_foreign.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_foreign.py b/tests/test_foreign.py index a3db0b869..df45281bb 100644 --- a/tests/test_foreign.py +++ b/tests/test_foreign.py @@ -201,6 +201,13 @@ def testNCOL(self): "name" in g.vertex_attributes() and "weight" not in g.edge_attributes() ) + @unittest.skipIf(pd is None, "test case depends on Pandas") + def testNCOLWithDataFrame(self): + # Regression test for https://github.com/igraph/python-igraph/issues/446 + from pandas import DataFrame + df = DataFrame({'from': [1, 2], 'to': [2, 3]}) + self.assertRaises(TypeError, Graph.Read_Ncol, df) + def testLGL(self): with temporary_file( """\ From fd831b262239bd6add6b6dce512715da7ed10b73 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 14:21:22 +0200 Subject: [PATCH 0447/1681] test: remove a debug statement from a test case --- tests/test_structural.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_structural.py b/tests/test_structural.py index eaeef42d6..8db1ed194 100644 --- a/tests/test_structural.py +++ b/tests/test_structural.py @@ -686,7 +686,6 @@ def testMaximumCardinalitySearch(self): g = Graph.Famous("petersen") alpha, alpham1 = g.maximum_cardinality_search() - print(repr(alpha), repr(alpham1)) self.assert_valid_maximum_cardinality_search_result(g, alpha, alpham1) g = Graph.GRG(100, 0.2) From 633c4bd4b09cda3326d4cdbe800d470e4fc0e4ff Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 14:21:53 +0200 Subject: [PATCH 0448/1681] chore: ignore more virtualenv folders --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 323291f0c..2391ed900 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ src/igraph/*.so .eggs/ .tox .venv/ +.venv-*/ .vscode/ vendor/build/ vendor/install/ From 53258b0fb73f0fd302725861de853f922f7f062d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 14:56:57 +0200 Subject: [PATCH 0449/1681] refactor: code cleanup --- src/_igraph/filehandle.c | 7 +++++-- src/_igraph/graphobject.c | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/_igraph/filehandle.c b/src/_igraph/filehandle.c index f7bc72f38..1e0a9f3b2 100644 --- a/src/_igraph/filehandle.c +++ b/src/_igraph/filehandle.c @@ -33,6 +33,7 @@ static int igraphmodule_i_filehandle_init_cpython_3(igraphmodule_filehandle_t* h return 1; } + handle->fp = 0; handle->need_close = 0; handle->object = 0; @@ -85,7 +86,9 @@ static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* hand return 1; } + handle->fp = 0; handle->need_close = 0; + handle->object = 0; if (PyBaseString_Check(object)) { /* We have received a string; we need to open the file denoted by this @@ -116,6 +119,7 @@ static int igraphmodule_i_filehandle_init_pypy_3(igraphmodule_filehandle_t* hand /* This already called Py_DECREF(handle->object), no need to call it */ return 1; } + handle->fp = fdopen(fp, mode); if (handle->fp == 0) { igraphmodule_filehandle_destroy(handle); @@ -156,9 +160,8 @@ void igraphmodule_filehandle_destroy(igraphmodule_filehandle_t* handle) { if (handle->need_close && !handle->object) { fclose(handle->fp); } + handle->fp = 0; } - - handle->fp = 0; if (handle->object != 0) { /* igraphmodule_PyFile_Close might mess up the stored exception, so let's diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index 39fcf0311..4b797c98f 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -2858,7 +2858,7 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, if (igraphmodule_PyObject_to_vector_t(outdeg_o, &outdeg, 0)) return NULL; - /* Indegree vector, PyNone means undirected graph */ + /* Indegree vector, Py_None means undirected graph */ if (indeg_o != Py_None) { if (igraphmodule_PyObject_to_vector_t(indeg_o, &indeg, 0)) { igraph_vector_destroy(&outdeg); @@ -2870,15 +2870,17 @@ PyObject *igraphmodule_Graph_Realize_Degree_Sequence(PyTypeObject *type, /* C function takes care of multi-sw and directed corner case */ if (igraph_realize_degree_sequence(&g, &outdeg, indegp, allowed_edge_types, method)) { igraph_vector_destroy(&outdeg); - if (indegp != 0) - igraph_vector_destroy(&indeg); + if (indegp != 0) { + igraph_vector_destroy(indegp); + } igraphmodule_handle_igraph_error(); return NULL; } igraph_vector_destroy(&outdeg); - if (indegp != 0) - igraph_vector_destroy(&indeg); + if (indegp != 0) { + igraph_vector_destroy(indegp); + } CREATE_GRAPH_FROM_TYPE(self, g, type); From 07d3969c723fa30b5c124bb93c4a9245bd7a3724 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 14:59:48 +0200 Subject: [PATCH 0450/1681] refactor: more code cleanup --- src/_igraph/convert.c | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 8b2330b57..2bfa6349d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -160,7 +160,7 @@ int igraphmodule_PyObject_to_enum(PyObject *o, * \param result the result is returned here. The default value must be * passed in before calling this function, since this value is * returned untouched if the given Python object is Py_None. - * \return 0 if everything is OK, 1 otherwise. An appropriate exception + * \return 0 if everything is OK, -1 otherwise. An appropriate exception * is raised in this case. */ int igraphmodule_PyObject_to_enum_strict(PyObject *o, @@ -168,33 +168,41 @@ int igraphmodule_PyObject_to_enum_strict(PyObject *o, int *result) { char *s, *s2; - if (o == 0 || o == Py_None) + if (o == 0 || o == Py_None) { return 0; - if (PyLong_Check(o)) + } + + if (PyLong_Check(o)) { return PyLong_AsInt(o, result); + } + s = PyUnicode_CopyAsString(o); if (s == 0) { - PyErr_SetString(PyExc_TypeError, "int, long or string expected"); - return -1; + PyErr_SetString(PyExc_TypeError, "int, long or string expected"); + return -1; } + /* Convert string to lowercase */ - for (s2=s; *s2; s2++) + for (s2 = s; *s2; s2++) { *s2 = tolower(*s2); + } + /* Search for exact matches */ while (table->name != 0) { - if (strcmp(s, table->name) == 0) { - *result = table->value; - free(s); - return 0; - } - table++; + if (strcmp(s, table->name) == 0) { + *result = table->value; + free(s); + return 0; + } + table++; } + free(s); PyErr_SetObject(PyExc_ValueError, o); + return -1; } - /** * \ingroup python_interface_conversion * \brief Converts a Python object to an igraph \c igraph_neimode_t From be01f0162caa67c14986de3ff68d40a607498c82 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 15:00:11 +0200 Subject: [PATCH 0451/1681] fix: fix crash in Graph.Realize_Degree_Sequence() on Python 3.7 (maybe other versions) --- src/_igraph/convert.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 2bfa6349d..2f8ee29ca 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -3143,6 +3143,7 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t * \brief Converts a Python object to an igraph \c igraph_edge_type_sw_t */ int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result) { + int result_int, retval; static igraphmodule_enum_translation_table_entry_t edge_type_sw_tt[] = { {"simple", IGRAPH_SIMPLE_SW}, {"loops", IGRAPH_LOOPS_SW}, @@ -3151,7 +3152,14 @@ int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t * {0,0} }; - return igraphmodule_PyObject_to_enum_strict(o, edge_type_sw_tt, (int*)result); + retval = igraphmodule_PyObject_to_enum_strict(o, edge_type_sw_tt, &result_int); + + if (retval) { + return retval; + } + + *result = result_int; + return 0; } /** From 78adc9e776eabfa7ba2f63602d5241a9040fc483 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 14 Oct 2021 15:12:26 +0200 Subject: [PATCH 0452/1681] fix: make sure that igraphmodule_PyObject_to_edge_type_sw_t() respects the default value passed in --- src/_igraph/convert.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_igraph/convert.c b/src/_igraph/convert.c index 2f8ee29ca..80fe02a8d 100644 --- a/src/_igraph/convert.c +++ b/src/_igraph/convert.c @@ -3143,7 +3143,8 @@ int igraphmodule_PyObject_to_pagerank_algo_t(PyObject *o, igraph_pagerank_algo_t * \brief Converts a Python object to an igraph \c igraph_edge_type_sw_t */ int igraphmodule_PyObject_to_edge_type_sw_t(PyObject *o, igraph_edge_type_sw_t *result) { - int result_int, retval; + int result_int = *result; + int retval; static igraphmodule_enum_translation_table_entry_t edge_type_sw_tt[] = { {"simple", IGRAPH_SIMPLE_SW}, {"loops", IGRAPH_LOOPS_SW}, From 1b665ae59bac96d9b81be80b6e72af3ef747f69d Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 Oct 2021 12:12:47 +0200 Subject: [PATCH 0453/1681] chore: updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a1472cd..462823bc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,10 @@ thanks to [@jboynyc](https://github.com/user/jboynyc). See PR [#439](https://github.com/igraph/python-igraph/pull/439) for more details. +- Fixed a bug that sometimes caused random crashes in + `Graph.Realize_Degree_Sequence()` and at other times caused weird errors in + `Graph.Read_Ncol()` when it received an invalid data type. + ## [0.9.6] ### Fixed From 477577961dfacc878c21454c86be81ba934515b9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 Oct 2021 12:22:11 +0200 Subject: [PATCH 0454/1681] chore: bumped version to 0.9.7 --- doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index eb5b48839..e94dd981f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.9.6' +version = '0.9.7' # The full version, including alpha/beta/rc tags. -release = '0.9.6' +release = '0.9.7' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index fd77afc92..752360a77 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 9, 6) +__version_info__ = (0, 9, 7) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 55f88b642b4a19910ea0ee1fd78c63be095ac9c9 Mon Sep 17 00:00:00 2001 From: Gwyn Ciesla Date: Fri, 15 Oct 2021 09:41:48 -0500 Subject: [PATCH 0455/1681] Fix for #448. --- setup.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 5d5138f0d..88daf42a2 100644 --- a/setup.py +++ b/setup.py @@ -378,14 +378,13 @@ def build_ext(self) -> Command: of igraph before compiling the Python extension. """ from setuptools.command.build_ext import build_ext - from distutils.sysconfig import get_python_inc buildcfg = self class custom_build_ext(build_ext): def run(self): # Bail out if we don't have the Python include files - include_dir = get_python_inc() + include_dir = sysconfig.get_path('include') if not os.path.isfile(os.path.join(include_dir, "Python.h")): print("You will need the Python headers to compile this extension.") sys.exit(1) From 4105e3c0c3a025cd37f3f77cd7863113431b6021 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 15 Oct 2021 17:34:30 +0200 Subject: [PATCH 0456/1681] chore: updated changelog --- CHANGELOG.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 462823bc7..6d6851f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ # igraph Python interface changelog -## [Unreleased] +## [0.9.8] + +### Fixed + +- `setup.py` no longer uses `distutils`, thanks to + [@limburgher](https://github.com/limburgher). + ([#449](https://github.com/igraph/python-igraph/pull/449)) + +## [0.9.7] ### Added @@ -201,7 +209,8 @@ Please refer to the commit logs at https://github.com/igraph/python-igraph for a list of changes affecting versions up to 0.8.3. Notable changes after 0.8.3 are documented above. -[unreleased]: https://github.com/igraph/python-igraph/compare/0.9.6..master +[unreleased]: https://github.com/igraph/python-igraph/compare/0.9.7..master +[0.9.7]: https://github.com/igraph/python-igraph/compare/0.9.6...0.9.7 [0.9.6]: https://github.com/igraph/python-igraph/compare/0.9.5...0.9.6 [0.9.5]: https://github.com/igraph/python-igraph/compare/0.9.4...0.9.5 [0.9.4]: https://github.com/igraph/python-igraph/compare/0.9.1...0.9.4 From cd22d6c5bd01fb037e17b5a38fab95f54fe82009 Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Wed, 13 Oct 2021 22:47:25 +0200 Subject: [PATCH 0457/1681] doc: Removed mentions of test in conda section. --- doc/source/install.rst | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 45c3759fc..a29159be3 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -61,7 +61,7 @@ The unit tests of `python-igraph` are implemented with the standard `unittest` m run them like this from your virtualenv: $ python -m unittest discover - + As usual, you can also do this without activating the virtualenv: $ venv/bin/python -m unittest discover @@ -75,14 +75,6 @@ using conda. That can be achieved by running the following command: $ conda install -c conda-forge python-igraph -To test the installed package, launch Python and run the following: - - >>> import igraph.test - >>> igraph.test.run_tests() - -The above commands run the bundled test cases to ensure that everything is fine with your -|igraph| installation. - |igraph| on Windows ------------------- From e42310eb0e64f93f8d8bc646dd8c43f33a09da6a Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Fri, 15 Oct 2021 12:20:18 +0200 Subject: [PATCH 0458/1681] doc: only explain tests when building from source. --- doc/source/install.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index a29159be3..11499ac65 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -57,15 +57,6 @@ in it directly: $ python -m venv venv $ venv/bin/pip install python-igraph -The unit tests of `python-igraph` are implemented with the standard `unittest` module so you can -run them like this from your virtualenv: - - $ python -m unittest discover - -As usual, you can also do this without activating the virtualenv: - - $ venv/bin/python -m unittest discover - Installing |igraph| via Conda ----------------------------- @@ -214,3 +205,11 @@ script to install it from there: $ python setup.py install .. note:: The `setup.py` script takes a number of options to customize the install location. + +Testing your installation +------------------------- + +The unit tests of `python-igraph` are implemented with the standard `unittest` module so you can +run them like this from your the source folder: + + $ python -m unittest discover From 18646292b068b38b12d94523733601d62f12a58f Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Fri, 15 Oct 2021 14:39:22 +0200 Subject: [PATCH 0459/1681] doc: corrected formatting for install. --- doc/source/install.rst | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 11499ac65..01e32fa7e 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -39,20 +39,20 @@ that you install it from the `Python Package Index and macOS. We aim to provide binary packages for the three latest minor versions of Python 3.x. To install |python-igraph| globally, use the following command (you probably need -administrator/root priviledges): +administrator/root priviledges):: $ pip install python-igraph Many users like to install packages into a project-specific `virtual environment `_. -A variation of the following commands should work on most platforms: +A variation of the following commands should work on most platforms:: $ python -m venv venv $ source venv/bin/activate $ pip install python-igraph -Alternatively, if you do not want to activate the virtualenv, you can call the `pip` executable -in it directly: +Alternatively, if you do not want to activate the virtualenv, you can call the ``pip`` executable +in it directly:: $ python -m venv venv $ venv/bin/pip install python-igraph @@ -62,7 +62,7 @@ Installing |igraph| via Conda Users of the `Anaconda Python distribution `_ or the `conda package manager `_ may opt to install |igraph|'s Python interface -using conda. That can be achieved by running the following command: +using conda. That can be achieved by running the following command:: $ conda install -c conda-forge python-igraph @@ -105,7 +105,7 @@ Graph plotting in |igraph| is implemented using a third-party package called `Ca on macOS, you must also install Cairo and its Python bindings. The Cairo project does not provide pre-compiled binaries for macOS, but the `Homebrew package manager `, so you can use it to install Cairo. After installing Homebrew itself, you -can run: +can run:: $ brew install cairo @@ -148,27 +148,27 @@ Both will be covered in the next sections. Compiling using pip ------------------- -If you want the development version of |python-igraph|, call: +If you want the development version of |python-igraph|, call:: $ pip install git+https://github.com/igraph/python-igraph -`pip` is smart enough to download the sources from Github, initialize the submodule for the |igraph| C core, +``pip`` is smart enough to download the sources from Github, initialize the submodule for the |igraph| C core, compile it, and then compile |python-igraph| against it and install it. As above, a virtual environment is a commonly used sandbox to test experimental packages. -If you want the latest release from PyPI but prefer to (or have to) install from source, call: +If you want the latest release from PyPI but prefer to (or have to) install from source, call:: $ pip install --no-binary ':all:' python-igraph -.. note:: If there is no binary for your system anyway, you can just try without the `--no-binary` option and +.. note:: If there is no binary for your system anyway, you can just try without the ``--no-binary`` option and obtain the same result. Compiling step by step ---------------------- This section should be rarely used in practice but explains how to compile and install |python-igraph| step -by step without `pip`. +by step without ``pip``. -First, obtain the bleeding-edge source code from Github: +First, obtain the bleeding-edge source code from Github:: $ git clone https://github.com/igraph/python-igraph.git @@ -176,40 +176,40 @@ or download a recent release from `PyPI `. Decompress the archive if needed. -Second, go into the folder: +Second, go into the folder:: $ cd python-igraph (it might have a slightly different name depending on the release). -Third, if you cloned the source from Github, initialize the `git` submodule for the |igraph| C core: +Third, if you cloned the source from Github, initialize the ``git`` submodule for the |igraph| C core:: $ git submodule update --init .. note:: If you prefer to compile and link |python-igraph| against an existing |igraph| C core, for instance - the one you installed with your package manager, you can skip the `git` submodule initialization step. If you - downloaded a tarball, you also need to remove the `vendor/source/igraph` folder because the setup script + the one you installed with your package manager, you can skip the ``git`` submodule initialization step. If you + downloaded a tarball, you also need to remove the ``vendor/source/igraph`` folder because the setup script will look for the vendored |igraph| copy first. However, a particular version of |python-igraph| is guaranteed - to work only with the version of the C core that is bundled with it (or with the revision that the `git` + to work only with the version of the C core that is bundled with it (or with the revision that the ``git`` submodule points to). -Fourth, call the standard Python `setup.py` script, e.g. for compiling: +Fourth, call the standard Python ``setup.py`` script, e.g. for compiling:: $ python setup.py build (press Enter when prompted). That will compile the |python-igraph| package in a subfolder called -`build/lib.`, e.g. `build/lib.linux-x86_64-3.8`. You can add -that folder to your `PYTHONPATH` if you want to import directly from it, or you can call the `setup.py` -script to install it from there: +``build/lib.``, e.g. `build/lib.linux-x86_64-3.8`. You can add +that folder to your ``PYTHONPATH`` if you want to import directly from it, or you can call the ``setup.py`` +script to install it from there:: $ python setup.py install -.. note:: The `setup.py` script takes a number of options to customize the install location. +.. note:: The ``setup.py`` script takes a number of options to customize the install location. Testing your installation ------------------------- -The unit tests of `python-igraph` are implemented with the standard `unittest` module so you can -run them like this from your the source folder: +The unit tests of ``python-igraph`` are implemented with the standard ``unittest`` module so you can +run them like this from your the source folder:: $ python -m unittest discover From 977547a4a7ba4ee31ba50ee47e4b8209f264848a Mon Sep 17 00:00:00 2001 From: Vincent Traag Date: Fri, 15 Oct 2021 14:40:55 +0200 Subject: [PATCH 0460/1681] doc: corrected link in install section. --- doc/source/install.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 01e32fa7e..290945aec 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -104,7 +104,7 @@ Graph plotting in |igraph| is implemented using a third-party package called `Ca `_. If you want to create publication-quality plots in |igraph| on macOS, you must also install Cairo and its Python bindings. The Cairo project does not provide pre-compiled binaries for macOS, but the `Homebrew package manager -`, so you can use it to install Cairo. After installing Homebrew itself, you +`_, so you can use it to install Cairo. After installing Homebrew itself, you can run:: $ brew install cairo @@ -172,8 +172,8 @@ First, obtain the bleeding-edge source code from Github:: $ git clone https://github.com/igraph/python-igraph.git -or download a recent release from `PyPI ` or from the -`Github releases page `. Decompress the archive if +or download a recent release from `PyPI `_ or from the +`Github releases page `_. Decompress the archive if needed. Second, go into the folder:: From 879c0a418d8dacaa2ea2c68073b035a5b3caad82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nepusz?= Date: Fri, 15 Oct 2021 17:51:09 +0200 Subject: [PATCH 0461/1681] doc: fix typo --- doc/source/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/install.rst b/doc/source/install.rst index 290945aec..c02177cf6 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -39,7 +39,7 @@ that you install it from the `Python Package Index and macOS. We aim to provide binary packages for the three latest minor versions of Python 3.x. To install |python-igraph| globally, use the following command (you probably need -administrator/root priviledges):: +administrator/root privileges):: $ pip install python-igraph From e79ac0a34143616273f35023c2d692ae14809284 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Sun, 17 Oct 2021 21:45:13 +0200 Subject: [PATCH 0462/1681] chore: mention Python 3.10 support in more places --- README.md | 8 ++++---- setup.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e276509f9..7c10bd183 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Build and test with tox](https://github.com/igraph/python-igraph/actions/workflows/build.yml/badge.svg)](https://github.com/igraph/python-igraph/actions/workflows/build.yml) -[![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9-blue)](https://pypi.python.org/pypi/python-igraph) +[![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue)](https://pypi.python.org/pypi/python-igraph) [![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) Python interface for the igraph library @@ -18,9 +18,9 @@ You can learn more about python-igraph [on our website](http://igraph.org/python ## Installation from PyPI We aim to provide wheels on PyPI for most of the stock Python versions; -typically the three most recent minor releases from Python 3.x. Therefore, -running the following command should work without having to compile anything -during installation: +typically at least the three most recent minor releases from Python 3.x. +Therefore, running the following command should work without having to compile +anything during installation: ``` pip install python-igraph diff --git a/setup.py b/setup.py index 88daf42a2..4fffda6a7 100644 --- a/setup.py +++ b/setup.py @@ -863,6 +863,7 @@ def use_educated_guess(self) -> None: "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Mathematics", From a7e8fa3ef3b8f3760aec389805e9dfa4e88963b9 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Mon, 18 Oct 2021 12:40:14 +0200 Subject: [PATCH 0463/1681] doc: markup fixes; code blocks should be indented and they should be preceded by '::', refs #447 --- doc/source/analysis.rst | 194 +++++++-------- doc/source/generation.rst | 99 ++++---- doc/source/install.rst | 4 +- doc/source/intro.rst | 1 - doc/source/misc.rst | 1 - doc/source/tutorial.rst | 444 ++++++++++++++++++----------------- doc/source/visualisation.rst | 111 ++++----- 7 files changed, 450 insertions(+), 404 deletions(-) diff --git a/doc/source/analysis.rst b/doc/source/analysis.rst index 79ee0a437..3fb657225 100644 --- a/doc/source/analysis.rst +++ b/doc/source/analysis.rst @@ -2,17 +2,18 @@ Graph analysis ============== + |igraph| enables analysis of graphs/networks from simple operations such as adding and removing nodes to complex theoretical constructs such as community detection. Read the `API documentation`_ for details on each function and class. -The context for the following examples will be to import |igraph| (commonly as `ig`), have the :class:`Graph` class and to have one or more graphs available: +The context for the following examples will be to import |igraph| (commonly as `ig`), have the :class:`Graph` class and to have one or more graphs available:: ->>> import igraph as ig ->>> from igraph import Graph ->>> g = Graph(edges=[[0, 1], [2, 3]]) + >>> import igraph as ig + >>> from igraph import Graph + >>> g = Graph(edges=[[0, 1], [2, 3]]) -To get a summary representation of the graph, use :meth:`Graph.summary`. For instance +To get a summary representation of the graph, use :meth:`Graph.summary`. For instance:: ->>> g.summary(verbosity=1) + >>> g.summary(verbosity=1) will provide a fairly detailed description. @@ -20,31 +21,32 @@ To copy a graph, use :meth:`Graph.copy`. This is a "shallow" copy: any mutable o If you want to copy a graph including all its attributes, use Python's `deepcopy` module. Vertices and edges -+++++++++++++++++++++++++++ +++++++++++++++++++ + Vertices are numbered 0 to `n-1`, where n is the number of vertices in the graph. These are called the "vertex ids". -To count vertices, use :meth:`Graph.vcount`: +To count vertices, use :meth:`Graph.vcount`:: ->>> n = g.vcount() + >>> n = g.vcount() -Edges also have ids from 0 to `m-1` and are counted by :meth:`Graph.ecount`: +Edges also have ids from 0 to `m-1` and are counted by :meth:`Graph.ecount`:: ->>> m = g.ecount() + >>> m = g.ecount() -To get a sequence of vertices, use their ids and :attr:`Graph.vs`: +To get a sequence of vertices, use their ids and :attr:`Graph.vs`:: ->>> for v in g.vs: ->>> print(v) + >>> for v in g.vs: + >>> print(v) -Similarly for edges, use :attr:`Graph.es`: +Similarly for edges, use :attr:`Graph.es`:: ->>> for e in g.es: ->>> print(e) + >>> for e in g.es: + >>> print(e) -You can index and slice vertices and edges similar to a list: +You can index and slice vertices and edges like indexing and slicing a list:: ->>> g.vs[:4] ->>> g.vs[0, 2, 4] ->>> g.es[3] + >>> g.vs[:4] + >>> g.vs[0, 2, 4] + >>> g.es[3] .. note:: The `vs` and `es` attributes are special sequences with their own useful methods. See `API documentation`_ for a full list. @@ -52,46 +54,47 @@ If you prefer a vanilla edge list, you can use :meth:`Graph.get_edge_list`. Incidence ++++++++++++++++++++++++++++++ -To get the vertices at the two ends of an edge, use :attr:`Edge.source` and :attr:`Edge.target`: +To get the vertices at the two ends of an edge, use :attr:`Edge.source` and :attr:`Edge.target`:: ->>> e = g.es[0] ->>> v1, v2 = e.source, e.target + >>> e = g.es[0] + >>> v1, v2 = e.source, e.target Vice versa, to get the edge if from the source and target vertices, you can use :meth:`Graph.get_eid` or, for multiple pairs of source/targets, :meth:`Graph.get_eids`. The boolean version, asking whether two vertices are directly connected, is :meth:`Graph.are_connected`. To get the edges incident on a vertex, you can use :meth:`Vertex.incident`, :meth:`Vertex.out_edges` and -:meth:`Vertex.in_edges`. The three are equivalent on undirected graphs but not directed ones, of course: +:meth:`Vertex.in_edges`. The three are equivalent on undirected graphs but not directed ones of course:: ->>> v = g.vs[0] ->>> edges = v.incident() + >>> v = g.vs[0] + >>> edges = v.incident() -The :meth:`Graph.incident` function fulfills the same purpose with a slightly different syntax based on vertex ids: +The :meth:`Graph.incident` function fulfills the same purpose with a slightly different syntax based on vertex IDs:: ->>> edges = g.incident(0) + >>> edges = g.incident(0) To get the full adjacency/incidence list representation of the graph, use :meth:`Graph.get_adjlist`, :meth:`Graph.g.get_inclist()` or, for a bipartite graph, :meth:`Graph.get_incidence`. Neighborhood -+++++++++++++ +++++++++++++ + To compute the neighbors, successors, and predecessors, the methods :meth:`Graph.neighbors`, :meth:`Graph.successors` and -:meth:`Graph.predecessors` are available. The three give the same answer in undirected graphs and have a similar dual syntax: +:meth:`Graph.predecessors` are available. The three give the same answer in undirected graphs and have a similar dual syntax:: ->>> neis = g.vs[0].neighbors() ->>> neis = g.neighbors(0) + >>> neis = g.vs[0].neighbors() + >>> neis = g.neighbors(0) -To get the list of vertices within a certain distance from one or more initial vertices, you can use :meth:`Graph.neighborhood`: +To get the list of vertices within a certain distance from one or more initial vertices, you can use :meth:`Graph.neighborhood`:: ->>> g.neighborhood([0, 1], order=2) + >>> g.neighborhood([0, 1], order=2) and to find the neighborhood size, there is :meth:`Graph.neighborhood_size`. Degrees +++++++ -To compute the degree, in-degree, or out-degree of a node, use :meth:`Vertex.degree`, :meth:`Vertex.indegree`, and :meth:`Vertex.outdegree`: +To compute the degree, in-degree, or out-degree of a node, use :meth:`Vertex.degree`, :meth:`Vertex.indegree`, and :meth:`Vertex.outdegree`:: ->>> deg = g.vs[0].degree() ->>> deg = g.degree(0) + >>> deg = g.vs[0].degree() + >>> deg = g.degree(0) To compute the max degree in a list of vertices, use :meth:`Graph.maxdegree`. @@ -100,81 +103,84 @@ To compute the max degree in a list of vertices, use :meth:`Graph.maxdegree`. Adding and removing vertices and edges ++++++++++++++++++++++++++++++++++++++ -To add nodes to a graph, use :meth:`Graph.add_vertex` and :meth:`Graph.add_vertices`: +To add nodes to a graph, use :meth:`Graph.add_vertex` and :meth:`Graph.add_vertices`:: ->>> g.add_vertex() ->>> g.add_vertices(5) + >>> g.add_vertex() + >>> g.add_vertices(5) This changes the graph `g` in place. You can specify the name of the vertices if you wish. -To remove nodes, use :meth:`Graph.delete_vertices`: +To remove nodes, use :meth:`Graph.delete_vertices`:: ->>> g.delete_vertices([1, 2]) + >>> g.delete_vertices([1, 2]) Again, you can specify the names or the actual :class:`Vertex` objects instead. -To add edges, use :meth:`Graph.add_edge` and :meth:`Graph.add_edges`: +To add edges, use :meth:`Graph.add_edge` and :meth:`Graph.add_edges`:: ->>> g.add_edge(0, 2) ->>> g.add_edges([(0, 2), (1, 3)]) + >>> g.add_edge(0, 2) + >>> g.add_edges([(0, 2), (1, 3)]) -To remove edges, use :meth:`Graph.delete_edges`: +To remove edges, use :meth:`Graph.delete_edges`:: ->>> g.delete_edges([0, 5]) # remove by edge id + >>> g.delete_edges([0, 5]) # remove by edge id You can also remove edges between source and target nodes. To contract vertices, use :meth:`Graph.contract_vertices`. Edges between contracted vertices will become loops. Graph operators -+++++++++++++++++ ++++++++++++++++ + It is possible to compute the union, intersection, difference, and other set operations (operators) between graphs. -To compute the union of the graphs (nodes/edges in either are kept): +To compute the union of the graphs (nodes/edges in either are kept):: ->>> gu = ig.union([g, g2, g3]) + >>> gu = ig.union([g, g2, g3]) -Similarly for the intersection (nodes/edges present in all are kept): +Similarly for the intersection (nodes/edges present in all are kept):: ->>> gu = ig.intersection([g, g2, g3]) + >>> gu = ig.intersection([g, g2, g3]) These two operations preserve attributes and can be performed with a few variations. The most important one is that vertices can be matched across the graphs by id (number) or by name. -These and other operations are also available as methods of the :class:`Graph` class: +These and other operations are also available as methods of the :class:`Graph` class:: ->>> g.union(g2) ->>> g.intersection(g2) ->>> g.disjoint_union(g2) ->>> g.difference(g2) ->>> g.complementer() # complement graph, same nodes but missing edges + >>> g.union(g2) + >>> g.intersection(g2) + >>> g.disjoint_union(g2) + >>> g.difference(g2) + >>> g.complementer() # complement graph, same nodes but missing edges -and even as numerical operators: +and even as numerical operators:: ->>> g |= g2 ->>> g_intersection = g and g2 + >>> g |= g2 + >>> g_intersection = g and g2 Topological sorting +++++++++++++++++++ -To sort a graph topologically, use :meth:`Graph.topological_sorting`: ->>> g = ig.Graph.Tree(10, 2, mode=ig.TREE_OUT) ->>> g.topological_sorting() +To sort a graph topologically, use :meth:`Graph.topological_sorting`:: + + >>> g = ig.Graph.Tree(10, 2, mode=ig.TREE_OUT) + >>> g.topological_sorting() Graph traversal -+++++++++++++++++++++ -A common operation is traversing the graph. |igraph| currently exposes breadth-first search (BFS) via :meth:`Graph.bfs` and :meth:`Graph.bfsiter`: ++++++++++++++++ ->>> [vertices, layers, parents] = g.bfs() ->>> it = g.bfsiter() # Lazy version +A common operation is traversing the graph. |igraph| currently exposes breadth-first search (BFS) via :meth:`Graph.bfs` and :meth:`Graph.bfsiter`:: -Depth-first search has a similar infrastructure via :meth:`Graph.dfs` and :meth:`Graph.dfsiter`: + >>> [vertices, layers, parents] = g.bfs() + >>> it = g.bfsiter() # Lazy version ->>> [vertices, parents] = g.dfs() ->>> it = g.dfsiter() # Lazy version +Depth-first search has a similar infrastructure via :meth:`Graph.dfs` and :meth:`Graph.dfsiter`:: -To perform a random walk from a certain vertex, use :meth:`Graph.random_walk`: + >>> [vertices, parents] = g.dfs() + >>> it = g.dfsiter() # Lazy version ->>> vids = g.random_walk(0, 3) +To perform a random walk from a certain vertex, use :meth:`Graph.random_walk`:: + + >>> vids = g.random_walk(0, 3) Pathfinding and cuts ++++++++++++++++++++ @@ -198,7 +204,8 @@ As well as functions related to cuts and paths: See also the section on flow. Global properties -+++++++++++++++++++++ ++++++++++++++++++ + A number of global graph measures are available. Basic: @@ -347,20 +354,20 @@ Clustering Simplification, permutations and rewiring +++++++++++++++++++++++++++++++++++++++++ -To check is a graph is simple, you can use :meth:`Graph.is_simple`. +To check is a graph is simple, you can use :meth:`Graph.is_simple`:: ->>> g.is_simple() + >>> g.is_simple() -To simplify a graph (remove multiedges and loops), use :meth:`Graph.simplify`: +To simplify a graph (remove multiedges and loops), use :meth:`Graph.simplify`:: ->>> g_simple = g.simplify() + >>> g_simple = g.simplify() To return a directed/undirected copy of the graph, use :meth:`Graph.as_directed` and :meth:`Graph.as_undirected`, respectively. -To permute the order of vertices, you can use :meth:`Graph.permute_vertices`: +To permute the order of vertices, you can use :meth:`Graph.permute_vertices`:: ->>> g = ig.Tree(6, 2) ->>> g_perm = g.permute_vertices([1, 0, 2, 3, 4, 5]) + >>> g = ig.Tree(6, 2) + >>> g_perm = g.permute_vertices([1, 0, 2, 3, 4, 5]) The canonical permutation can be obtained via :meth:`Graph.canonical_permutation`, which can then be directly passed to the function above. @@ -371,22 +378,23 @@ To rewire the graph at random, there are: Line graph ++++++++++ -To compute the line graph of a graph `g`, which represents the connectedness of the *edges* of g, you can use :meth:`Graph.linegraph`: ->>> g = Graph(n=4, edges=[[0, 1], [0, 2]]) ->>> gl = g.linegraph() +To compute the line graph of a graph `g`, which represents the connectedness of the *edges* of g, you can use :meth:`Graph.linegraph`:: + + >>> g = Graph(n=4, edges=[[0, 1], [0, 2]]) + >>> gl = g.linegraph() -In this case, the line graph has two vertices, representing the two edges of the original graph, and one edge, representing the point where -those two original edges touch. +In this case, the line graph has two vertices, representing the two edges of the original graph, and one edge, representing the point where those two original edges touch. Composition and subgraphs -++++++++++++++++++++++++++ ++++++++++++++++++++++++++ + The function :meth:`Graph.decompose` decomposes the graph into subgraphs. Vice versa, the function :meth:`Graph.compose` returns the composition of two graphs. -To compute the subgraph spannes by some vertices/edges, use :meth:`Graph.subgraph` (aka :meth:`Graph.induced_subgraph`) and :meth:`Graph.subgraph_edges`: +To compute the subgraph spannes by some vertices/edges, use :meth:`Graph.subgraph` (aka :meth:`Graph.induced_subgraph`) and :meth:`Graph.subgraph_edges`:: ->>> g_sub = g.subgraph([0, 1]) ->>> g_sub = g.subgraph_edges([0]) + >>> g_sub = g.subgraph([0, 1]) + >>> g_sub = g.subgraph_edges([0]) To compute the minimum spanning tree, use :meth:`Graph.spanning_tree`. @@ -397,7 +405,8 @@ The dominator tree from a given node can be obtained with :meth:`Graph.dominator Bipartite graphs can be decomposed using :meth:`Graph.bipartite_projection`. The size of the projections can be computed using :meth:`Graph.bipartite_projection_size`. Morphisms -++++++++++++++++++ ++++++++++ + |igraph| enables comparisons between graphs: - :meth:`Graph.isomorphic` @@ -414,6 +423,7 @@ Morphisms Flow ++++ + Flow is a characteristic of directed graphs. The following functions are available: - :meth:`Graph.maxflow` between two nodes diff --git a/doc/source/generation.rst b/doc/source/generation.rst index d41da27d4..47e048e90 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -5,29 +5,32 @@ Graph generation The first step of most |igraph| applications is to generate a graph. This section will explain a number of ways to do that. Read the `API documentation`_ for details on each function and class. -The :class:`Graph` class is the main object used to generate graphs: +The :class:`Graph` class is the main object used to generate graphs:: ->>> from igraph import Graph + >>> from igraph import Graph -To copy a graph, use :meth:`Graph.copy`: +To copy a graph, use :meth:`Graph.copy`:: ->>> g_new = g.copy() + >>> g_new = g.copy() From nodes and edges ++++++++++++++++++++ + Nodes are always numbered from 0 upwards. To create a generic graph with a specified number of nodes (e.g. 10) and a list of edges between them, you can use the generic constructor: ->>> g = Graph(n=10, edges=[[0, 1], [2, 3]]) + >>> g = Graph(n=10, edges=[[0, 1], [2, 3]]) -If not specified, the graph is undirected. To make a directed graph: +If not specified, the graph is undirected. To make a directed graph:: ->>> g = Graph(n=10, edges=[[0, 1], [2, 3]], directed=True) + >>> g = Graph(n=10, edges=[[0, 1], [2, 3]], directed=True) -To specify edge weights (or any other vertex/edge attributes), use dictionaries: +To specify edge weights (or any other vertex/edge attributes), use dictionaries:: ->>> g = Graph(n=4, edges=[[0, 1], [2, 3]], ->>> edge_attrs={'weight': [0.1, 0.2]}, ->>> vertex_attrs={'color': ['b', 'g', 'g', 'y']}) + >>> g = Graph( + ... n=4, edges=[[0, 1], [2, 3]], + ... edge_attrs={'weight': [0.1, 0.2]}, + ... vertex_attrs={'color': ['b', 'g', 'g', 'y']} + ... ) Variations on this constructor is :meth:`Graph.DictList`, which constructs a graph from a list-of-dictionaries representation, and :meth:`Graph.TupleList`, which constructs a graph from a list-of-tuples representation. @@ -35,71 +38,79 @@ To create a bipartite graph from a list of types and a list of edges, use :meth: From matrices +++++++++++++ -To create a graph from an adjecency matrix, use :meth:`Graph.Adjacency` or, for weighted matrices, :meth:`Graph.Weighted_Adjacency`: +To create a graph from an adjecency matrix, use :meth:`Graph.Adjacency` or, for weighted matrices, :meth:`Graph.Weighted_Adjacency`:: ->>> g = Graph.Adjacency([[0, 1, 1], [0, 0, 0], [0, 0, 1]]) + >>> g = Graph.Adjacency([[0, 1, 1], [0, 0, 0], [0, 0, 1]]) This graph is directed and has edges `[0, 1]`, `[0, 2]` and `[2, 2]` (a loop). -To create a bipartite graph from an incidence matrix, use :meth:`Graph.Incidence`: +To create a bipartite graph from an incidence matrix, use :meth:`Graph.Incidence`:: ->>> g = Graph.Incidence([[0, 1, 1], [1, 1, 0]]) + >>> g = Graph.Incidence([[0, 1, 1], [1, 1, 0]]) From file +++++++++ -To load a graph from a preexisting file in any of the supported formats, use :meth:`Graph.Load`. For instance: ->>> g = Graph.Load('myfile.gml', format='gml') +To load a graph from a preexisting file in any of the supported formats, use :meth:`Graph.Load`. For instance:: + + >>> g = Graph.Load('myfile.gml', format='gml') If you don't specify a format, |igraph| will try to figure it out or, if that fails, it will complain. From external libraries +++++++++++++++++++++++ -|igraph| can read from and write to `networkx` and `graph-tool` graph formats: ->>> g = Graph.from_networkx(nwx) +|igraph| can read from and write to `networkx` and `graph-tool` graph formats:: + + >>> g = Graph.from_networkx(nwx) and ->>> g = Graph.from_graph_tool(gt) +:: + + >>> g = Graph.from_graph_tool(gt) From pandas DataFrame(s) ++++++++++++++++++++++++ + A common practice is to store edges in a `pandas.DataFrame`, where the two first columns are the source and target vertex ids, -and any additional column indicates edge attributes. You can generate a graph via :meth:`Graph.DataFrame`: +and any additional column indicates edge attributes. You can generate a graph via :meth:`Graph.DataFrame`:: ->>> g = Graph.DataFrame(edges, directed=False) + >>> g = Graph.DataFrame(edges, directed=False) It is possible to set vertex attributes at the same time via a separate DataFrame. The first column is assumed to contain all -vertex ids (including any vertices without edges) and any additional columns are vertex attributes: +vertex ids (including any vertices without edges) and any additional columns are vertex attributes:: ->>> g = Graph.DataFrame(edges, directed=False, vertices=vertices) + >>> g = Graph.DataFrame(edges, directed=False, vertices=vertices) From a formula ++++++++++++++ -To create a graph from a string formula, use :meth:`Graph.Formula`, e.g.: ->>> g = Graph.Formula('D-A:B:F:G, A-C-F-A, B-E-G-B, A-B, F-G, H-F:G, H-I-J') +To create a graph from a string formula, use :meth:`Graph.Formula`, e.g.:: + + >>> g = Graph.Formula('D-A:B:F:G, A-C-F-A, B-E-G-B, A-B, F-G, H-F:G, H-I-J') .. note:: This particular formula also assigns the 'name' attribute to vertices. Full graphs +++++++++++ -To create a full graph, use :meth:`Graph.Full`: ->>> g = Graph.Full(n=3) +To create a full graph, use :meth:`Graph.Full`:: + + >>> g = Graph.Full(n=3) -where `n` is the number of nodes. You can specify directedness and whether self loops are allowed: +where `n` is the number of nodes. You can specify directedness and whether self loops are allowed:: ->>> g = Graph.Full(n=3, directed=True, loops=True) + >>> g = Graph.Full(n=3, directed=True, loops=True) A similar method, :meth:`Graph.Full_Bipartite`, generates a full bipartite graph. Finally, the metho :meth:`Graph.Full_Citation` created the full citation graph, in which each vertex `i` has a directed edge to all vertices strictly smaller than `i`. Tree and star +++++++++++++ -:meth:`Graph.Tree` can be used to generate regular trees, in which almost each vertex has the same number of children: ->>> g = Graph.Tree(n=7, n_children=2) +:meth:`Graph.Tree` can be used to generate regular trees, in which almost each vertex has the same number of children:: + + >>> g = Graph.Tree(n=7, n_children=2) creates a tree with seven vertices - of which four are leaves. The root (0) has two children (1 and 2), each of which has two children (the four leaves). Regular trees can be directed or undirected (default). @@ -107,21 +118,23 @@ The method :meth:`Graph.Star` creates a star graph, which is a subtype of a tree Lattice +++++++ -:meth:`Graph.Lattice` creates a regular lattice of the chosen size. For instance: ->>> g = Graph.Lattice(dim=[3, 3], circular=False) +:meth:`Graph.Lattice` creates a regular lattice of the chosen size. For instance:: + + >>> g = Graph.Lattice(dim=[3, 3], circular=False) creates a 3x3 grid in two dimensions (9 vertices total). `circular` is used to connect each edge of the lattice back onto the other side, a process also known as "periodic boundary condition" that is sometimes helpful to smoothen out edge effects. -The one dimensional case (path graph or ring) is important enough to deserve its own constructor :meth:`Graph.Ring`, which can be circular or not: +The one dimensional case (path graph or ring) is important enough to deserve its own constructor :meth:`Graph.Ring`, which can be circular or not:: ->>> g = Graph.Ring(n=4, circular=False) + >>> g = Graph.Ring(n=4, circular=False) -Graph atlas +Graph Atlas +++++++++++ -The book ‘An Atlas of Graphs’ by Roland C. Read and Robin J. Wilson contains all undirected graphs with up to seven vertices, numbered from 0 up to 1252. You can create any graph from this list by index with :meth:`Graph.Atlas`, e.g.: ->>> g = Graph.Atlas(44) +The book ‘An Atlas of Graphs’ by Roland C. Read and Robin J. Wilson contains all undirected graphs with up to seven vertices, numbered from 0 up to 1252. You can create any graph from this list by index with :meth:`Graph.Atlas`, e.g.:: + + >>> g = Graph.Atlas(44) The graphs are listed: @@ -130,12 +143,12 @@ The graphs are listed: - for fixed numbers of nodes and edges, in increasing order of the degree sequence, for example 111223 < 112222; - for fixed degree sequence, in increasing number of automorphisms. - Famous graphs +++++++++++++ -A curated list of famous graphs, which are often used in the literature for benchmarking and other purposes, is available on the `igraph C core manual `_. You can generate any graph in that list by name, e.g.: ->>> g = Graph.Famous('Zachary') +A curated list of famous graphs, which are often used in the literature for benchmarking and other purposes, is available on the `igraph C core manual `_. You can generate any graph in that list by name, e.g.:: + + >>> g = Graph.Famous('Zachary') will teach you some about martial arts. @@ -162,9 +175,9 @@ Stochastic graphs can be created according to several different models or games: - random graph with a given degree sequence :meth:`Graph.Degree_Sequence` - bipartite :meth:`Graph.Random_Bipartite` - Other graphs ++++++++++++ + Finally, there are some ways of generating graphs that are not covered by the previous sections: - Kautz graphs :meth:`Graph.Kautz` diff --git a/doc/source/install.rst b/doc/source/install.rst index c02177cf6..f786c12d6 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -73,8 +73,6 @@ Precompiled Windows wheels for |igraph|'s Python interface are available on the `_ (see `Installing igraph from the Python Package Index`_). -TODO: Check if Windows still requires special steps to get PyCairo running. - Graph plotting in |igraph| is implemented using a third-party package called `Cairo `_. If you want to create publication-quality plots in |igraph| on Windows, you must also install Cairo and its Python bindings. The Cairo project does not @@ -148,6 +146,7 @@ Both will be covered in the next sections. Compiling using pip ------------------- + If you want the development version of |python-igraph|, call:: $ pip install git+https://github.com/igraph/python-igraph @@ -165,6 +164,7 @@ If you want the latest release from PyPI but prefer to (or have to) install from Compiling step by step ---------------------- + This section should be rarely used in practice but explains how to compile and install |python-igraph| step by step without ``pip``. diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 34db9acf7..8ed105ac9 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -14,4 +14,3 @@ Things you should know before starting out Reporting bugs and providing feedback ===================================== - diff --git a/doc/source/misc.rst b/doc/source/misc.rst index 966896a45..65cd44207 100644 --- a/doc/source/misc.rst +++ b/doc/source/misc.rst @@ -2,4 +2,3 @@ Miscellaneous topics ==================== .. note:: TODO. This is a placeholder section; it is not written yet. - diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index 854c9195b..57e972502 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -32,18 +32,18 @@ ordinary Python module at the Python prompt:: This imports |igraph|'s objects and methods inside an own namespace called :mod:`igraph`. Whenever you would like to call any of |igraph|'s methods, you will have to provide the appropriate namespace-qualification. E.g., to check which |igraph| version you are using, you could do the -following: +following:: ->>> import igraph ->>> print(igraph.__version__) -0.9.6 + >>> import igraph + >>> print(igraph.__version__) + 0.9.6 Another way to make use of |igraph| is to import all its objects and methods into the main Python namespace (so you do not have to type the namespace-qualification every time). This is fine as long as your own objects and methods do not conflict with the ones -provided by |igraph|: +provided by |igraph|:: ->>> from igraph import * + >>> from igraph import * The third way to start |igraph| is to simply call the startup script that was supplied with the |igraph| package you installed. Not too surprisingly, the script is called :command:`igraph`, @@ -88,32 +88,34 @@ Creating a graph from scratch ============================= Assuming that you have started |igraph| successfully, it is time to create your first -|igraph| graph. This is pretty simple: +|igraph| graph. This is pretty simple:: ->>> g = Graph() + >>> g = Graph() The above statement created an undirected graph with no vertices or edges and assigned it to the variable `g`. To confirm that it's really an |igraph| graph, we can -print it: +print it:: ->>> g - + >>> g + This tells us that `g` is an instance of |igraph|'s :class:`Graph` class and that it is currently living at the memory address ``0x4c87a0`` (the exact output will almost surely be different for your platform). To obtain a more user-friendly output, we can try to print the graph using Python's -``print`` statement: +``print`` statement:: ->>> print(g) -IGRAPH U--- 0 0 -- + >>> print(g) + IGRAPH U--- 0 0 -- This summary consists of `IGRAPH`, followed by a four-character long code, the number of vertices, the number of edges, two dashes (`--`) and the name of the graph (i.e. the contents of the `name` attribute, if any) This is not too exciting so far; a graph with no vertices and no edges is not really useful for us. Let's add some vertices first! ->>> g.add_vertices(3) +:: + + >>> g.add_vertices(3) :meth:`Graph.add_vertices` (i.e., the :meth:`~Graph.add_vertices` method of the :class:`Graph` class) adds the given number of vertices to the graph. @@ -125,20 +127,20 @@ first vertex of your graph has index zero, the second vertex has index 1 and so Edges are specified by pairs of integers, so ``[(0,1), (1,2)]`` denotes a list of two edges: one between the first and the second, and the other one between the second and the third vertices of the graph. Passing this list to :meth:`Graph.add_edges` adds these two edges -to your graph: +to your graph:: ->>> g.add_edges([(0,1), (1,2)]) + >>> g.add_edges([(0,1), (1,2)]) :meth:`~Graph.add_edges` is clever enough to figure out what you want to do in most of the cases: if you supply a single pair of integers, it will automatically assume that you want to add a single edge. However, if you try to add edges to vertices with invalid IDs (i.e., you try to add an edge to vertex 5 when you only have three vertices), you will get an -exception: +exception:: ->>> g.add_edges((5, 0)) -Traceback (most recent call last): - File "", line 6, in -TypeError: iterable must return pairs of integers or strings + >>> g.add_edges((5, 0)) + Traceback (most recent call last): + File "", line 6, in + TypeError: iterable must return pairs of integers or strings Most |igraph| functions will raise an :exc:`igraph.InternalError` if something goes wrong. The message corresponding to the exception gives you a @@ -148,15 +150,15 @@ occurred. The exact filename and line number may not be too informative to you, but it is invaluable for |igraph| developers if you think you found an error in |igraph| and you want to report it. -Let us go on with our graph ``g`` and add some more vertices and edges to it: +Let us go on with our graph ``g`` and add some more vertices and edges to it:: ->>> g.add_edges([(2, 0)]) ->>> g.add_vertices(3) ->>> g.add_edges([(2, 3), (3, 4), (4, 5), (5, 3)]) ->>> print(g) -IGRAPH U---- 6 7 -- -+ edges: -0--1 1--2 0--2 2--3 3--4 4--5 3--5 + >>> g.add_edges([(2, 0)]) + >>> g.add_vertices(3) + >>> g.add_edges([(2, 3), (3, 4), (4, 5), (5, 3)]) + >>> print(g) + IGRAPH U---- 6 7 -- + + edges: + 0--1 1--2 0--2 2--3 3--4 4--5 3--5 Now, this is better. We have an undirected graph with six vertices and seven edges, and you can also see the list of edges in |igraph|'s output. Edges also @@ -174,11 +176,13 @@ vertices at its two endpoints, you can use :meth:`~Graph.get_eid` to get the edge ID. Remember, all these are *methods* of the :class:`Graph` class and you must call them on the appropriate :class:`Graph` instance! ->>> g.get_eid(2, 3) -3 ->>> g.delete_edges(3) ->>> summary(g) -IGRAPH U--- 6 6 -- +:: + + >>> g.get_eid(2, 3) + 3 + >>> g.delete_edges(3) + >>> summary(g) + IGRAPH U--- 6 6 -- :meth:`summary` is a new command that you haven't seen before; it is a member of |igraph|'s own namespace and it can be used to get an overview of a given graph object. Its output @@ -198,20 +202,20 @@ creating trees, regular lattices, rings, extended chordal rings, several famous and so on, while stochastic generators are used to create Erdős-Rényi random networks, Barabási-Albert networks, geometric random graphs and such. |igraph| has too many generators to cover them all in this tutorial, so we will only try a -deterministic and a stochastic generator instead: +deterministic and a stochastic generator instead:: ->>> g = Graph.Tree(127, 2) ->>> summary(g) -IGRAPH U--- 127 126 -- + >>> g = Graph.Tree(127, 2) + >>> summary(g) + IGRAPH U--- 127 126 -- :meth:`Graph.Tree` generates a regular tree graph. The one that we generated has 127 vertices and each vertex (apart from the leaves) has two children (and of course one parent). No matter how many times you call :meth:`Graph.Tree`, the generated graph will -always be the same if you use the same parameters: +always be the same if you use the same parameters:: ->>> g2 = Graph.Tree(127, 2) ->>> g2.get_edgelist() == g.get_edgelist() -True + >>> g2 = Graph.Tree(127, 2) + >>> g2.get_edgelist() == g.get_edgelist() + True The above code snippet also shows you that the :meth:`~Graph.get_edgelist()` method of :class:`Graph` graph objects return a list that contains pairs of integers, one for @@ -219,15 +223,19 @@ each edge. The first member of the pair is the source vertex ID and the second m is the target vertex ID of the corresponding edge. This list is too long, so let's just print the first 10 elements! ->>> g2.get_edgelist()[0:10] -[(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6), (3, 7), (3, 8), (4, 9), (4, 10)] +:: + + >>> g2.get_edgelist()[0:10] + [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6), (3, 7), (3, 8), (4, 9), (4, 10)] Let's do the same with a stochastic generator! ->>> g = Graph.GRG(100, 0.2) ->>> summary(g) -IGRAPH U---- 100 516 -- -+ attr: x (v), y (v) +:: + + >>> g = Graph.GRG(100, 0.2) + >>> summary(g) + IGRAPH U---- 100 516 -- + + attr: x (v), y (v) where ``+ attr`` shows names of the attributes for vertices (v) and edges (e). @@ -238,13 +246,13 @@ the random nature of the algorithm, chances are that the exact graph you got is from the one that was generated when I wrote this tutorial, hence the values above in the summary will not match the ones you got. This is normal and expected. Even if you generate two geometric random graphs on the same machine, they will be different for the same parameter -set: +set:: ->>> g2 = Graph.GRG(100, 0.2) ->>> g.get_edgelist() == g2.get_edgelist() -False ->>> g.isomorphic(g2) -False + >>> g2 = Graph.GRG(100, 0.2) + >>> g.get_edgelist() == g2.get_edgelist() + False + >>> g.isomorphic(g2) + False :meth:`~Graph.isomorphic()` tells you whether two graphs are isomorphic or not. In general, it might take quite a lot of time, especially for large graphs, but in our case, the @@ -280,21 +288,23 @@ and the value representing the attribute itself. Let us create a simple imaginary social network the usual way by hand. ->>> g = Graph([(0,1), (0,2), (2,3), (3,4), (4,2), (2,5), (5,0), (6,3), (5,6)]) +:: + + >>> g = Graph([(0,1), (0,2), (2,3), (3,4), (4,2), (2,5), (5,0), (6,3), (5,6)]) Now, let us assume that we want to store the names, ages and genders of people in this network as vertex attributes, and for every connection, we want to store whether this is an informal friendship tie or a formal tie. Every :class:`Graph` object contains two special members called :attr:`~Graph.vs` and :attr:`~Graph.es`, standing for the sequence of all vertices and all edges, respectively. If you try to use :attr:`~Graph.vs` or :attr:`~Graph.es` as -a Python dictionary, you will manipulate the attribute storage area of the graph: +a Python dictionary, you will manipulate the attribute storage area of the graph:: ->>> g.vs - ->>> g.vs["name"] = ["Alice", "Bob", "Claire", "Dennis", "Esther", "Frank", "George"] ->>> g.vs["age"] = [25, 31, 18, 47, 22, 23, 50] ->>> g.vs["gender"] = ["f", "m", "f", "m", "f", "m", "m"] ->>> g.es["is_formal"] = [False, False, True, True, True, False, True, False, False] + >>> g.vs + + >>> g.vs["name"] = ["Alice", "Bob", "Claire", "Dennis", "Esther", "Frank", "George"] + >>> g.vs["age"] = [25, 31, 18, 47, 22, 23, 50] + >>> g.vs["gender"] = ["f", "m", "f", "m", "f", "m", "m"] + >>> g.es["is_formal"] = [False, False, True, True, True, False, True, False, False] Whenever you use :attr:`~Graph.vs` or :attr:`~Graph.es` as a dictionary, you are assigning attributes to *all* vertices/edges of the graph. However, you can simply alter the attributes @@ -303,15 +313,15 @@ with integers as if they were lists (remember, they are sequences, they contain vertices or all the edges). When you index them, you obtain a :class:`Vertex` or :class:`Edge` object, which refers to (I am sure you already guessed that) a single vertex or a single edge of the graph. :class:`Vertex` and :class:`Edge` objects can also be used -as dictionaries to alter the attributes of that single vertex or edge: +as dictionaries to alter the attributes of that single vertex or edge:: ->>> g.es[0] -igraph.Edge(,0,{'is_formal': False}) ->>> g.es[0].attributes() -{'is_formal': False} ->>> g.es[0]["is_formal"] = True ->>> g.es[0] -igraph.Edge(,0,{'is_formal': True}) + >>> g.es[0] + igraph.Edge(,0,{'is_formal': False}) + >>> g.es[0].attributes() + {'is_formal': False} + >>> g.es[0]["is_formal"] = True + >>> g.es[0] + igraph.Edge(,0,{'is_formal': True}) The above snippet illustrates that indexing an :class:`EdgeSeq` object returns :class:`Edge` objects; the representation above shows the graph the object belongs to, @@ -332,23 +342,23 @@ we will see methods that can filter :class:`EdgeSeq` objects and return other :class:`EdgeSeq` objects that are restricted to a subset of edges, and of course the same applies to :class:`VertexSeq` objects. But before we dive into that, let's see how we can assign attributes to the whole graph. Not too surprisingly, :class:`Graph` objects -themselves can also behave as dictionaries: +themselves can also behave as dictionaries:: ->>> g["date"] = "2009-01-10" ->>> print(g["date"]) -2009-01-10 + >>> g["date"] = "2009-01-10" + >>> print(g["date"]) + 2009-01-10 Finally, it should be mentioned that attributes can be deleted by the Python keyword -``del`` just as you would do with any member of an ordinary dictionary: +``del`` just as you would do with any member of an ordinary dictionary:: ->>> g.vs[3]["foo"] = "bar" ->>> g.vs["foo"] -[None, None, None, 'bar', None, None, None] ->>> del g.vs["foo"] ->>> g.vs["foo"] -Traceback (most recent call last): - File "", line 25, in -KeyError: 'Attribute does not exist' + >>> g.vs[3]["foo"] = "bar" + >>> g.vs["foo"] + [None, None, None, 'bar', None, None, None] + >>> del g.vs["foo"] + >>> g.vs["foo"] + Traceback (most recent call last): + File "", line 25, in + KeyError: 'Attribute does not exist' Structural properties of graphs =============================== @@ -363,20 +373,20 @@ Probably the simplest property one can think of is the :dfn:`vertex degree`. The degree of a vertex equals the number of edges adjacent to it. In case of directed networks, we can also define :dfn:`in-degree` (the number of edges pointing towards the vertex) and :dfn:`out-degree` (the number of edges originating from the vertex). -|igraph| is able to calculate all of them using a simple syntax: +|igraph| is able to calculate all of them using a simple syntax:: ->>> g.degree() -[3, 1, 4, 3, 2, 3, 2] + >>> g.degree() + [3, 1, 4, 3, 2, 3, 2] If the graph was directed, we would have been able to calculate the in- and out-degrees separately using ``g.degree(mode="in")`` and ``g.degree(mode="out")``. You can also pass a single vertex ID or a list of vertex IDs to :meth:`~Graph.degree` if you -want to calculate the degrees for only a subset of vertices: +want to calculate the degrees for only a subset of vertices:: ->>> g.degree(6) -2 ->>> g.degree([2,3,4]) -[4, 3, 2] + >>> g.degree(6) + 2 + >>> g.degree([2,3,4]) + [4, 3, 2] This calling convention applies to most of the structural properties |igraph| can calculate. For vertex properties, the methods accept a vertex ID or a list of vertex IDs @@ -397,30 +407,30 @@ restrict them to exactly the vertices or edges you want. Besides degree, |igraph| includes built-in routines to calculate many other centrality properties, including vertex and edge betweenness (:meth:`Graph.betweenness`, :meth:`Graph.edge_betweenness`) or Google's PageRank (:meth:`Graph.pagerank`) -just to name a few. Here we just illustrate edge betweenness: +just to name a few. Here we just illustrate edge betweenness:: ->>> g.edge_betweenness() -[6.0, 6.0, 4.0, 2.0, 4.0, 3.0, 4.0, 3.0. 4.0] + >>> g.edge_betweenness() + [6.0, 6.0, 4.0, 2.0, 4.0, 3.0, 4.0, 3.0. 4.0] Now we can also figure out which connections have the highest betweenness centrality -with some Python magic: +with some Python magic:: ->>> ebs = g.edge_betweenness() ->>> max_eb = max(ebs) ->>> [g.es[idx].tuple for idx, eb in enumerate(ebs) if eb == max_eb] -[(0, 1), (0, 2)] + >>> ebs = g.edge_betweenness() + >>> max_eb = max(ebs) + >>> [g.es[idx].tuple for idx, eb in enumerate(ebs) if eb == max_eb] + [(0, 1), (0, 2)] Most structural properties can also be retrieved for a subset of vertices or edges or for a single vertex or edge by calling the appropriate method on the :class:`VertexSeq`, :class:`EdgeSeq`, :class:`Vertex` or :class:`Edge` object of -interest: +interest:: ->>> g.vs.degree() -[3, 1, 4, 3, 2, 3, 2] ->>> g.es.edge_betweenness() -[6.0, 6.0, 4.0, 2.0, 4.0, 3.0, 4.0, 3.0. 4.0] ->>> g.vs[2].degree() -4 + >>> g.vs.degree() + [3, 1, 4, 3, 2, 3, 2] + >>> g.es.edge_betweenness() + [6.0, 6.0, 4.0, 2.0, 4.0, 3.0, 4.0, 3.0. 4.0] + >>> g.vs[2].degree() + 4 .. _querying_vertices_and_edges: @@ -433,10 +443,10 @@ Selecting vertices and edges Imagine that in a given social network, you would like to find out who has the largest degree or betweenness centrality. You can do that with the tools presented so far and some basic Python knowledge, but since it is a common task to select vertices and edges -based on attributes or structural properties, |igraph| gives you an easier way to do that: +based on attributes or structural properties, |igraph| gives you an easier way to do that:: ->>> g.vs.select(_degree=g.maxdegree())["name"] -["Alice", "Bob"] + >>> g.vs.select(_degree=g.maxdegree())["name"] + ["Alice", "Bob"] The syntax may seem a little bit awkward for the first sight, so let's try to interpret it step by step. :meth:`~VertexSeq.select` is a method of :class:`VertexSeq` and its @@ -446,50 +456,50 @@ arguments. Positional arguments (the ones without an explicit name like ``_degree`` above) are always processed before keyword arguments as follows: - If the first positional argument is ``None``, an empty sequence (containing no - vertices) is returned: + vertices) is returned:: - >>> seq = g.vs.select(None) - >>> len(seq) - 0 + >>> seq = g.vs.select(None) + >>> len(seq) + 0 - If the first positional argument is a callable object (i.e., a function, a bound method or anything that behaves like a function), the object will be called for every vertex that's currently in the sequence. If the function returns ``True``, - the vertex will be included, otherwise it will be excluded: + the vertex will be included, otherwise it will be excluded:: - >>> graph = Graph.Full(10) - >>> only_odd_vertices = graph.vs.select(lambda vertex: vertex.index % 2 == 1) - >>> len(only_odd_vertices) - 5 + >>> graph = Graph.Full(10) + >>> only_odd_vertices = graph.vs.select(lambda vertex: vertex.index % 2 == 1) + >>> len(only_odd_vertices) + 5 - If the first positional argument is an iterable (i.e., a list, a generator or anything that can be iterated over), it *must* return integers and these integers will be considered as indices into the current vertex set (which is *not* necessarily the whole graph). Only those vertices that match the given indices will be included in the filtered vertex set. Floats, strings, invalid vertex IDs will silently be - ignored: - - >>> seq = graph.vs.select([2, 3, 7]) - >>> len(seq) - 3 - >>> [v.index for v in seq] - [2, 3, 7] - >>> seq = seq.select([0, 2]) # filtering an existing vertex set - >>> [v.index for v in seq] - [2, 7] - >>> seq = graph.vs.select([2, 3, 7, "foo", 3.5]) - >>> len(seq) - 3 + ignored:: + + >>> seq = graph.vs.select([2, 3, 7]) + >>> len(seq) + 3 + >>> [v.index for v in seq] + [2, 3, 7] + >>> seq = seq.select([0, 2]) # filtering an existing vertex set + >>> [v.index for v in seq] + [2, 7] + >>> seq = graph.vs.select([2, 3, 7, "foo", 3.5]) + >>> len(seq) + 3 - If the first positional argument is an integer, all remaining arguments are also expected to be integers and they are interpreted as indices into the current vertex set. This is just syntactic sugar, you could achieve an equivalent effect by passing a list as the first positional argument, but this way you can omit the - square brackets: + square brackets:: - >>> seq = graph.vs.select(2, 3, 7) - >>> len(seq) - 3 + >>> seq = graph.vs.select(2, 3, 7) + >>> len(seq) + 3 Keyword arguments can be used to filter the vertices based on their attributes or their structural properties. The name of each keyword argument should consist @@ -528,9 +538,9 @@ Keyword argument Meaning ================ ================================================================ For instance, the following command gives you people younger than 30 years in -our imaginary social network: +our imaginary social network:: ->>> g.vs.select(age_lt=30) + >>> g.vs.select(age_lt=30) .. note:: Due to the syntactical constraints of Python, you cannot use the admittedly @@ -538,26 +548,26 @@ our imaginary social network: allowed to appear in an argument list in Python. To save you some typing, you can even omit the :meth:`~VertexSeq.select` method if -you wish: +you wish:: ->>> g.vs(age_lt=30) + >>> g.vs(age_lt=30) Theoretically, it can happen that there exists an attribute and a structural property with the same name (e.g., you could have a vertex attribute named ``degree``). In that case, we would not be able to decide whether the user meant ``degree`` as a structural property or as a vertex attribute. To resolve this ambiguity, structural property names *must* always be preceded by an underscore (``_``) when used for filtering. For example, to -find vertices with degree larger than 2: +find vertices with degree larger than 2:: ->>> g.vs(_degree_gt=2) + >>> g.vs(_degree_gt=2) There are also a few special structural properties for selecting edges: - Using ``_source`` or ``_from`` in the keyword argument list of :meth:`EdgeSeq.select` filters based on the source vertices of the edges. E.g., to select all the edges - originating from Claire (who has vertex index 2): + originating from Claire (who has vertex index 2):: - >>> g.es.select(_source=2) + >>> g.es.select(_source=2) - Using ``_target`` or ``_to`` filters based on the target vertices. This is different from ``_source`` and ``_from`` if the graph is directed. @@ -565,22 +575,22 @@ There are also a few special structural properties for selecting edges: - ``_within`` takes a :class:`VertexSeq` object or a list or set of vertex indices and selects all the edges that originate and terminate in the given vertex set. For instance, the following expression selects all the edges between - Claire (vertex index 2), Dennis (vertex index 3) and Esther (vertex index 4): + Claire (vertex index 2), Dennis (vertex index 3) and Esther (vertex index 4):: - >>> g.es.select(_within=[2,3,4]) + >>> g.es.select(_within=[2,3,4]) - We could also have used a :class:`VertexSeq` object: + We could also have used a :class:`VertexSeq` object:: - >>> g.es.select(_within=g.vs[2:5]) + >>> g.es.select(_within=g.vs[2:5]) - ``_between`` takes a tuple consisting of two :class:`VertexSeq` objects or lists containing vertex indices or :class:`Vertex` objects and selects all the edges that originate in one of the sets and terminate in the other. E.g., to select all the - edges that connect men to women: + edges that connect men to women:: - >>> men = g.vs.select(gender="m") - >>> women = g.vs.select(gender="f") - >>> g.es.select(_between=(men, women)) + >>> men = g.vs.select(gender="m") + >>> women = g.vs.select(gender="f") + >>> g.es.select(_between=(men, women)) Finding a single vertex or edge with some properties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -593,20 +603,20 @@ looking up vertices by their names in the ``name`` property. :class:`VertexSeq` :meth:`~VertexSeq.find` works similarly to :meth:`~VertexSeq.select`, but it returns only the first match if there are multiple matches, and throws an exception if no match is found. For instance, to look up the vertex corresponding to Claire, one can -do this: +do this:: ->>> claire = g.vs.find(name="Claire") ->>> type(claire) -igraph.Vertex ->>> claire.index -2 + >>> claire = g.vs.find(name="Claire") + >>> type(claire) + igraph.Vertex + >>> claire.index + 2 -Looking up an unknown name will yield an exception: +Looking up an unknown name will yield an exception:: ->>> g.vs.find(name="Joe") -Traceback (most recent call last): - File "", line 1, in -ValueError: no such vertex + >>> g.vs.find(name="Joe") + Traceback (most recent call last): + File "", line 1, in + ValueError: no such vertex Looking up vertices by names ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -618,15 +628,15 @@ can be looked up by their names in amortized constant time. To make things even |igraph| accepts vertex names (almost) anywhere where it expects vertex IDs, and also accepts collections (list, tuples etc) of vertex names anywhere where it expects lists of vertex IDs or :class:`VertexSeq` instances. E.g, you can simply look up the degree -(number of connections) of Dennis as follows: +(number of connections) of Dennis as follows:: ->>> g.degree("Dennis") -3 + >>> g.degree("Dennis") + 3 or, alternatively: ->>> g.vs.find("Dennis").degree() -3 + >>> g.vs.find("Dennis").degree() + 3 The mapping between vertex names and IDs is maintained transparently by |igraph| in the background; whenever the graph changes, |igraph| also updates the internal mapping. @@ -640,8 +650,18 @@ Treating a graph as an adjacency matrix Adjacency matrix is another way to form a graph. In adjacency matrix, rows and columns are labeled by graph vertices: the elements of the matrix indicate whether the vertices *i* and *j* have a common edge (*i, j*). The adjacency matrix for the example graph is ->>> g.get_adjacency() -Matrix([[0, 1, 1, 0, 0, 1, 0], [1, 0, 0, 0, 0, 0, 0], [1, 0, 0, 1, 1, 1, 0], [0, 0, 1, 0, 1, 0, 1], [0, 0, 1, 1, 0, 0, 0], [1, 0, 1, 0, 0, 0, 1], [0, 0, 0, 1, 0, 1, 0]]) +:: + + >>> g.get_adjacency() + Matrix([ + [0, 1, 1, 0, 0, 1, 0], + [1, 0, 0, 0, 0, 0, 0], + [1, 0, 0, 1, 1, 1, 0], + [0, 0, 1, 0, 1, 0, 1], + [0, 0, 1, 1, 0, 0, 0], + [1, 0, 1, 0, 0, 0, 1], + [0, 0, 0, 1, 0, 1, 0] + ]) For example, Claire (``[1, 0, 0, 1, 1, 1, 0]``) is directly connected to Alice (who has vertex index 0), Dennis (index 3), Esther (index 4), and Frank (index 5), but not to Bob (index 1) nor George (index 6). @@ -711,18 +731,18 @@ Method name Short name Algorithm description .. _Large Graph Layout: http://sourceforge.net/projects/lgl/ Layout algorithms can either be called directly or using the common layout method called -:meth:`~Graph.layout`: +:meth:`~Graph.layout`:: ->>> layout = g.layout_kamada_kawai() ->>> layout = g.layout("kamada_kawai") + >>> layout = g.layout_kamada_kawai() + >>> layout = g.layout("kamada_kawai") The first argument of the :meth:`~Graph.layout` method must be the short name of the layout algorithm (see the table above). All the remaining positional and keyword arguments are passed intact to the chosen layout method. For instance, the following two calls are -completely equivalent: +completely equivalent:: ->>> layout = g.layout_reingold_tilford(root=[2]) ->>> layout = g.layout("rt", [2]) + >>> layout = g.layout_reingold_tilford(root=[2]) + >>> layout = g.layout("rt", [2]) Layout methods return a :class:`Layout` object which behaves mostly like a list of lists. Each list entry in a :class:`Layout` object corresponds to a vertex in the original graph @@ -735,10 +755,10 @@ Drawing a graph using a layout ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For instance, we can plot our imaginary social network with the Kamada-Kawai -layout algorithm as follows: +layout algorithm as follows:: ->>> layout = g.layout("kk") ->>> plot(g, layout=layout) + >>> layout = g.layout("kk") + >>> plot(g, layout=layout) This should open an external image viewer showing a visual representation of the network, something like the one on the following figure (although the exact placement of @@ -751,22 +771,22 @@ nodes may be different on your machine since the layout is not deterministic): Our social network with the Kamada-Kawai layout algorithm If you prefer to use `matplotlib`_ as a plotting engine, create an axes and use the -``target`` argument: +``target`` argument:: ->>> import matplotlib.pyplot as plt ->>> fig, ax = plt.subplots() ->>> plot(g, layout=layout, target=ax) + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> plot(g, layout=layout, target=ax) Hmm, this is not too pretty so far. A trivial addition would be to use the names as the vertex labels and to color the vertices according to the gender. Vertex labels are taken from the ``label`` attribute by default and vertex colors are determined by the -``color`` attribute, so we can simply create these attributes and re-plot the graph: +``color`` attribute, so we can simply create these attributes and re-plot the graph:: ->>> g.vs["label"] = g.vs["name"] ->>> color_dict = {"m": "blue", "f": "pink"} ->>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] ->>> plot(g, layout=layout, bbox=(300, 300), margin=20) ->>> plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib version + >>> g.vs["label"] = g.vs["name"] + >>> color_dict = {"m": "blue", "f": "pink"} + >>> g.vs["color"] = [color_dict[gender] for gender in g.vs["gender"]] + >>> plot(g, layout=layout, bbox=(300, 300), margin=20) + >>> plot(g, layout=layout, bbox=(300, 300), margin=20, target=ax) # matplotlib version Note that we are simply re-using the previous layout object here, but we also specified that we need a smaller plot (300 x 300 pixels) and a larger margin around the graph @@ -779,10 +799,10 @@ to fit the labels (20 pixels). The result is: Our social network - with names as labels and genders as colors Instead of specifying the visual properties as vertex and edge attributes, you can -also give them as keyword arguments to :func:`plot`: +also give them as keyword arguments to :func:`plot`:: ->>> color_dict = {"m": "blue", "f": "pink"} ->>> plot(g, layout=layout, vertex_color=[color_dict[gender] for gender in g.vs["gender"]]) + >>> color_dict = {"m": "blue", "f": "pink"} + >>> plot(g, layout=layout, vertex_color=[color_dict[gender] for gender in g.vs["gender"]]) This latter approach is preferred if you want to keep the properties of the visual representation of your graph separate from the graph itself. You can simply set up @@ -790,15 +810,15 @@ a Python dictionary containing the keyword arguments you would pass to :func:`pl and then use the double asterisk (``**``) operator to pass your specific styling attributes to :func:`plot`:: ->>> visual_style = {} ->>> visual_style["vertex_size"] = 20 ->>> visual_style["vertex_color"] = [color_dict[gender] for gender in g.vs["gender"]] ->>> visual_style["vertex_label"] = g.vs["name"] ->>> visual_style["edge_width"] = [1 + 2 * int(is_formal) for is_formal in g.es["is_formal"]] ->>> visual_style["layout"] = layout ->>> visual_style["bbox"] = (300, 300) ->>> visual_style["margin"] = 20 ->>> plot(g, **visual_style) + >>> visual_style = {} + >>> visual_style["vertex_size"] = 20 + >>> visual_style["vertex_color"] = [color_dict[gender] for gender in g.vs["gender"]] + >>> visual_style["vertex_label"] = g.vs["name"] + >>> visual_style["edge_width"] = [1 + 2 * int(is_formal) for is_formal in g.es["is_formal"]] + >>> visual_style["layout"] = layout + >>> visual_style["bbox"] = (300, 300) + >>> visual_style["margin"] = 20 + >>> plot(g, **visual_style) The final plot shows the formal ties with thick lines while informal ones with thin lines: @@ -955,7 +975,7 @@ SVG or PDF files can then later be converted to PostScript (``.ps``) or Encapsul PostScript (``.eps``) format if you prefer that, while PNG files can be converted to TIF (``.tif``):: ->>> plot(g, "social_network.pdf", **visual_style) + >>> plot(g, "social_network.pdf", **visual_style) |igraph| and the outside world @@ -1012,16 +1032,16 @@ As an exercise, download the graph representation of the well-known `Zachary karate club study `_ from :download:`this file `, unzip it and try to load it into |igraph|. Since it is a GraphML file, you must use the GraphML reader method from -the table above (make sure you use the appropriate path to the downloaded file): +the table above (make sure you use the appropriate path to the downloaded file):: ->>> karate = Graph.Read_GraphML("zachary.graphml") ->>> summary(karate) -IGRAPH UNW- 34 78 -- Zachary's karate club network + >>> karate = Graph.Read_GraphML("zachary.graphml") + >>> summary(karate) + IGRAPH UNW- 34 78 -- Zachary's karate club network If you want to convert the very same graph into, say, Pajek's format, you can do it -with the Pajek writer method from the table above: +with the Pajek writer method from the table above:: ->>> karate.write_pajek("zachary.net") + >>> karate.write_pajek("zachary.net") .. note:: Most of the formats have their own limitations; for instance, not all of them can store attributes. Your best bet is probably GraphML or GML if you @@ -1038,11 +1058,11 @@ reader methods which tries to infer the appropriate format from the file extensi :meth:`Graph.save` is the opposite of :func:`load`: it lets you save a graph where the preferred format is again inferred from the extension. The format detection of :func:`load` and :meth:`Graph.save` can be overridden by the ``format`` keyword -argument which accepts the short names of the formats from the above table: +argument which accepts the short names of the formats from the above table:: ->>> karate = load("zachary.graphml") ->>> karate.save("zachary.net") ->>> karate.save("zachary.my_extension", format="gml") + >>> karate = load("zachary.graphml") + >>> karate.save("zachary.net") + >>> karate.save("zachary.my_extension", format="gml") Where to go next diff --git a/doc/source/visualisation.rst b/doc/source/visualisation.rst index a67fc90be..a7ce000b2 100644 --- a/doc/source/visualisation.rst +++ b/doc/source/visualisation.rst @@ -6,10 +6,10 @@ Visualisation of graphs |igraph| includes functionality to visualize graphs. There are two main components: graph layouts and graph plotting. In the following examples, we will assume |igraph| is imported as `ig` and a -:class:`Graph` object has been previously created, e.g.: +:class:`Graph` object has been previously created, e.g.:: ->>> import igraph as ig ->>> g = ig.Graph(edges=[[0, 1], [2, 3]]) + >>> import igraph as ig + >>> g = ig.Graph(edges=[[0, 1], [2, 3]]) Read the `API documentation`_ for details on each function and class. The `tutorial`_ contains examples to get started. @@ -19,9 +19,9 @@ A graph *layout* is a low-dimensional (usually: 2 dimensional) representation of graph can be computed and typically preserve or highlight distinct properties of the graph itself. Some layouts only make sense for specific kinds of graphs, such as trees. -|igraph| offers several graph layouts. The general function to compute a graph layout is :meth:`Graph.layout`: +|igraph| offers several graph layouts. The general function to compute a graph layout is :meth:`Graph.layout`:: ->>> layout = g.layout(layout='auto') + >>> layout = g.layout(layout='auto') See below for a list of supported layouts. The resulting object is an instance of `igraph.layout.Layout` and has some useful properties: @@ -35,14 +35,14 @@ and methods: - :meth:`Layout.bounding_box` the boundary, but as an `igraph.drawing.utils.BoundingBox` (see below) - :meth:`Layout.centroid` the coordinates of the centroid of the graph layout -Indexing and slicing can be performed and returns the coordinates of the requested vertices: +Indexing and slicing can be performed and returns the coordinates of the requested vertices:: ->>> coords_subgraph = layout[:2] # Coordinates of the first two vertices + >>> coords_subgraph = layout[:2] # Coordinates of the first two vertices .. note:: The returned object is a list of lists with the coordinates, not an `igraph.layout.Layout` object. You can wrap the result into such an object easily: - - >>> layout_subgraph = ig.Layout(coords=layout[:2]) + + >>> layout_subgraph = ig.Layout(coords=layout[:2]) It is possible to perform linear transformations to the layout: @@ -94,111 +94,116 @@ function, `igraph.plot`. Plotting with the default image viewer ++++++++++++++++++++++++++++++++++++++ -A naked call to `igraph.plot` generates a temporary file and opens it with the default image viewer: ->>> ig.plot(g) +A naked call to `igraph.plot` generates a temporary file and opens it with the default image viewer:: + + >>> ig.plot(g) (see below if you are using this in a `Jupyter`_ notebook). This uses the `Cairo`_ library behind the scenes. Saving a plot to a file +++++++++++++++++++++++ + A call to `igraph.plot` with a `target` argument stores the graph image in the specified file and does *not* open it automatically. Based on the filename extension, any of the following output formats can be chosen: -PNG, PDF, SVG and PostScript: +PNG, PDF, SVG and PostScript:: ->>> ig.plot(g, target='myfile.pdf') + >>> ig.plot(g, target='myfile.pdf') .. note:: PNG is a raster image format while PDF, SVG, and Postscript are vector image formats. Choose one of the last three formats if you are planning on refining the image with a vector image editor such as Inkscape or Illustrator. Plotting graphs within Matplotlib figures +++++++++++++++++++++++++++++++++++++++++ -If the target argument is a `matplotlib`_ axes, the graph will be plotted inside that axes: ->>> import matplotlib.pyplot as plt ->>> fig, ax = plt.subplots() ->>> ig.plot(g, target=ax) +If the target argument is a `matplotlib`_ axes, the graph will be plotted inside that axes:: + + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> ig.plot(g, target=ax) You can then further manipulate the axes and figure however you like via the `ax` and `fig` variables (or whatever you called them). This variant does not use `Cairo`_ directly and might be lacking some features that are available in the `Cairo`_ backend: please open an issue on Github to request specific features. Plotting via `matplotlib`_ makes it easy to combine igraph with other plots. For instance, if you want to have a figure -with two panels showing different aspects of some data set, say a graph and a bar plot, you can easily do that: +with two panels showing different aspects of some data set, say a graph and a bar plot, you can easily do that:: ->>> import matplotlib.pyplot as plt ->>> fig, axs = plt.subplots(1, 2, figsize=(8, 4)) ->>> ig.plot(g, target=axs[0]) ->>> axs[1].bar(x=[0, 1, 2], height=[1, 5, 3], color='tomato') + >>> import matplotlib.pyplot as plt + >>> fig, axs = plt.subplots(1, 2, figsize=(8, 4)) + >>> ig.plot(g, target=axs[0]) + >>> axs[1].bar(x=[0, 1, 2], height=[1, 5, 3], color='tomato') Another common situation is modifying the graph plot after the fact, to achieve some kind of customization. For instance, -you might want to change the size and color of the vertices: +you might want to change the size and color of the vertices:: ->>> import matplotlib.pyplot as plt ->>> fig, ax = plt.subplots() ->>> ig.plot(g, target=ax) ->>> dots = ax.get_children()[0] # This is a PathCollection ->>> dots.set_color('tomato') ->>> dots.set_sizes([250] * g.vcount()) + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> ig.plot(g, target=ax) + >>> dots = ax.get_children()[0] # This is a PathCollection + >>> dots.set_color('tomato') + >>> dots.set_sizes([250] * g.vcount()) That also helps as a workaround if you cannot figure out how to use the plotting options below: just use the defaults and then customize the appearance of your graph via standard `matplotlib`_ tools. Plotting graphs in Jupyter notebooks ++++++++++++++++++++++++++++++++++++ + |igraph| supports inline plots within a `Jupyter`_ notebook via both the `Cairo`_ and `matplotlib`_ backend. If you are calling `igraph.plot` from a notebook cell without a `matplotlib`_ axes, the image will be shown inline in the corresponding output cell. Use the `bbox` argument to scale the image while preserving the size of the vertices, text, and other artists. -For instance, to get a compact plot: +For instance, to get a compact plot:: ->>> ig.plot(g, bbox=(0, 0, 100, 100)) + >>> ig.plot(g, bbox=(0, 0, 100, 100)) These inline plots can be either in PNG or SVG format. There is currently an open bug that makes SVG fail if more than one graph per notebook is plotted: we are working on a fix for that. In the meanwhile, you can use PNG representation. If you want to use the `matplotlib`_ engine in a Jupyter notebook, you can use the recipe above. First create an axes, then -tell `igraph.plot` about it via the `target` argument: +tell `igraph.plot` about it via the `target` argument:: ->>> import matplotlib.pyplot as plt ->>> fig, ax = plt.subplots() ->>> ig.plot(g, target=ax) + >>> import matplotlib.pyplot as plt + >>> fig, ax = plt.subplots() + >>> ig.plot(g, target=ax) Exporting to other graph formats ++++++++++++++++++++++++++++++++++ If igraph is missing a certain plotting feature and you cannot wait for us to include it, you can always export your graph to a number of formats and use an external graph plotting tool. We support both conversion to file (e.g. DOT format used by -`graphviz`_) and to popular graph libraries such as `networkx`_ and `graph-tool`_: +`graphviz`_) and to popular graph libraries such as `networkx`_ and `graph-tool`_:: ->>> dot = g.write('/myfolder/myfile.dot') ->>> n = g.to_networkx() ->>> gt = g.to_graph_tool() + >>> dot = g.write('/myfolder/myfile.dot') + >>> n = g.to_networkx() + >>> gt = g.to_graph_tool() You do not need to have any libraries installed if you export to file, but you do need them to convert directly to external Python objects (`networkx`_, `graph-tool`_). Plotting options -==================== -You can add an argument `layout` to the `plot` function to specify a precomputed layout, e.g.: +================ + +You can add an argument `layout` to the `plot` function to specify a precomputed layout, e.g.:: ->>> layout = g.layout("kamada_kawai") ->>> ig.plot(g, layout=layout) + >>> layout = g.layout("kamada_kawai") + >>> ig.plot(g, layout=layout) -It is also possible to use the name of the layout algorithm directly: +It is also possible to use the name of the layout algorithm directly:: ->>> ig.plot(g, layout="kamada_kawai") + >>> ig.plot(g, layout="kamada_kawai") If the layout is left unspecified, igraph uses the dedicated `layout_auto()` function, which chooses between one of several possible layouts based on the number of vertices and edges. -You can also specify vertex and edge color, size, and labels - and more - via additional arguments, e.g.: +You can also specify vertex and edge color, size, and labels - and more - via additional arguments, e.g.:: ->>> ig.plot(g, ->>> vertex_size=20, ->>> vertex_color=['blue', 'red', 'green', 'yellow'], ->>> vertex_label=['first', 'second', 'third', 'fourth'], ->>> edge_width=[1, 4], ->>> edge_color=['black', 'grey'], ->>> ) + >>> ig.plot(g, + ... vertex_size=20, + ... vertex_color=['blue', 'red', 'green', 'yellow'], + ... vertex_label=['first', 'second', 'third', 'fourth'], + ... edge_width=[1, 4], + ... edge_color=['black', 'grey'], + ... ) See the `tutorial`_ for examples and a full list of options. From b32c47c08b71ebe356a3baa9e64c5236ce08071c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Wed, 20 Oct 2021 17:21:50 +0200 Subject: [PATCH 0464/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 2ceb15db7..b29e741ea 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 2ceb15db7983a15e499844daddd1e1aa72cb0138 +Subproject commit b29e741ea4106b259318d1d1d37404d317b2a185 From c4e50cbad72817e0d7edd76b1a2cd63bd0339769 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 12:57:39 +0200 Subject: [PATCH 0465/1681] chore: minor setup.py tweaks --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4fffda6a7..c4944ee0b 100644 --- a/setup.py +++ b/setup.py @@ -818,6 +818,7 @@ def use_educated_guess(self) -> None: author_email="ntamas@gmail.com", project_urls={ "Bug Tracker": "https://github.com/igraph/python-igraph/issues", + "Changelog": "https://github.com/igraph/python-igraph/blob/master/CHANGELOG.md", "CI": "https://github.com/igraph/python-igraph/actions", "Documentation": "https://igraph.org/python/doc", "Source Code": "https://github.com/igraph/python-igraph", @@ -858,12 +859,13 @@ def use_educated_guess(self) -> None: "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: C", - "Programming Language :: Python", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Mathematics", From 9f93b0379d27f2c27b68d8b7fb979da217a66eed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szabolcs=20Horva=CC=81t?= Date: Fri, 22 Oct 2021 16:02:06 +0200 Subject: [PATCH 0466/1681] docs: mention the keyword "cycle" when discussion ring graphs in the tutorial --- doc/source/generation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/source/generation.rst b/doc/source/generation.rst index 47e048e90..5aa01a669 100644 --- a/doc/source/generation.rst +++ b/doc/source/generation.rst @@ -38,6 +38,7 @@ To create a bipartite graph from a list of types and a list of edges, use :meth: From matrices +++++++++++++ + To create a graph from an adjecency matrix, use :meth:`Graph.Adjacency` or, for weighted matrices, :meth:`Graph.Weighted_Adjacency`:: >>> g = Graph.Adjacency([[0, 1, 1], [0, 0, 0], [0, 0, 1]]) @@ -119,13 +120,13 @@ The method :meth:`Graph.Star` creates a star graph, which is a subtype of a tree Lattice +++++++ -:meth:`Graph.Lattice` creates a regular lattice of the chosen size. For instance:: +:meth:`Graph.Lattice` creates a regular square lattice of the chosen size. For instance:: >>> g = Graph.Lattice(dim=[3, 3], circular=False) creates a 3x3 grid in two dimensions (9 vertices total). `circular` is used to connect each edge of the lattice back onto the other side, a process also known as "periodic boundary condition" that is sometimes helpful to smoothen out edge effects. -The one dimensional case (path graph or ring) is important enough to deserve its own constructor :meth:`Graph.Ring`, which can be circular or not:: +The one dimensional case (path graph or cycle graph) is important enough to deserve its own constructor :meth:`Graph.Ring`, which can be circular or not:: >>> g = Graph.Ring(n=4, circular=False) @@ -155,6 +156,7 @@ will teach you some about martial arts. Random graphs +++++++++++++ + Stochastic graphs can be created according to several different models or games: - Barabasi-Albert model: :meth:`Graph.Barabasi` From 27a0bff14e17f57b37886f2fa905d52147ebf73a Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 2 Oct 2021 15:04:43 +1000 Subject: [PATCH 0467/1681] Community module --- src/igraph/__init__.py | 487 +++---------------------------- src/igraph/community/__init__.py | 478 ++++++++++++++++++++++++++++++ 2 files changed, 517 insertions(+), 448 deletions(-) create mode 100644 src/igraph/community/__init__.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 94541a63e..7a53fd765 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -75,6 +75,20 @@ set_status_handler, __igraph_version__, ) +from igraph.community import ( + _community_fastgreedy, + _community_infomap, + _community_leading_eigenvector_naive, + _community_leading_eigenvector, + _community_label_propagation, + _community_multilevel, + _community_optimal_modularity, + _community_edge_betweenness, + _community_spinglass, + _community_walktrap, + _k_core, + _community_leiden, +) from igraph.clustering import ( Clustering, VertexClustering, @@ -1195,464 +1209,29 @@ def get_automorphisms_vf2( ) # Various clustering algorithms -- mostly wrappers around GraphBase - def community_fastgreedy(self, weights=None): - """Community structure based on the greedy optimization of modularity. - - This algorithm merges individual nodes into communities in a way that - greedily maximizes the modularity score of the graph. It can be proven - that if no merge can increase the current modularity score, the - algorithm can be stopped since no further increase can be achieved. - - This algorithm is said to run almost in linear time on sparse graphs. - - @param weights: edge attribute name or a list containing edge - weights - @return: an appropriate L{VertexDendrogram} object. - - @newfield ref: Reference - @ref: A Clauset, MEJ Newman and C Moore: Finding community structure - in very large networks. Phys Rev E 70, 066111 (2004). - """ - merges, qs = GraphBase.community_fastgreedy(self, weights) - - # qs may be shorter than |V|-1 if we are left with a few separated - # communities in the end; take this into account - diff = self.vcount() - len(qs) - qs.reverse() - if qs: - optimal_count = qs.index(max(qs)) + diff + 1 - else: - optimal_count = diff - - return VertexDendrogram( - self, merges, optimal_count, modularity_params=dict(weights=weights) - ) - - def community_infomap(self, edge_weights=None, vertex_weights=None, trials=10): - """Finds the community structure of the network according to the Infomap - method of Martin Rosvall and Carl T. Bergstrom. - - @param edge_weights: name of an edge attribute or a list containing - edge weights. - @param vertex_weights: name of an vertex attribute or a list containing - vertex weights. - @param trials: the number of attempts to partition the network. - @return: an appropriate L{VertexClustering} object with an extra attribute - called C{codelength} that stores the code length determined by the - algorithm. - - @newfield ref: Reference - @ref: M. Rosvall and C. T. Bergstrom: Maps of information flow reveal - community structure in complex networks, PNAS 105, 1118 (2008). - U{http://dx.doi.org/10.1073/pnas.0706851105}, - U{http://arxiv.org/abs/0707.0609}. - @ref: M. Rosvall, D. Axelsson, and C. T. Bergstrom: The map equation, - Eur. Phys. J. Special Topics 178, 13 (2009). - U{http://dx.doi.org/10.1140/epjst/e2010-01179-1}, - U{http://arxiv.org/abs/0906.1405}. - """ - membership, codelength = GraphBase.community_infomap( - self, edge_weights, vertex_weights, trials - ) - return VertexClustering( - self, - membership, - params={"codelength": codelength}, - modularity_params={"weights": edge_weights}, - ) - - def community_leading_eigenvector_naive(self, clusters=None, return_merges=False): - """Naive implementation of Newman's eigenvector community structure detection. - - This function splits the network into two components - according to the leading eigenvector of the modularity matrix and - then recursively takes the given number of steps by splitting the - communities as individual networks. This is not the correct way, - however, see the reference for explanation. Consider using the - correct L{community_leading_eigenvector} method instead. - - @param clusters: the desired number of communities. If C{None}, the - algorithm tries to do as many splits as possible. Note that the - algorithm won't split a community further if the signs of the leading - eigenvector are all the same, so the actual number of discovered - communities can be less than the desired one. - @param return_merges: whether the returned object should be a - dendrogram instead of a single clustering. - @return: an appropriate L{VertexClustering} or L{VertexDendrogram} - object. - - @newfield ref: Reference - @ref: MEJ Newman: Finding community structure in networks using the - eigenvectors of matrices, arXiv:physics/0605087""" - if clusters is None: - clusters = -1 - cl, merges, q = GraphBase.community_leading_eigenvector_naive( - self, clusters, return_merges - ) - if merges is None: - return VertexClustering(self, cl, modularity=q) - else: - return VertexDendrogram(self, merges, safemax(cl) + 1) - - def community_leading_eigenvector( - self, clusters=None, weights=None, arpack_options=None - ): - """Newman's leading eigenvector method for detecting community structure. - - This is the proper implementation of the recursive, divisive algorithm: - each split is done by maximizing the modularity regarding the - original network. - - @param clusters: the desired number of communities. If C{None}, the - algorithm tries to do as many splits as possible. Note that the - algorithm won't split a community further if the signs of the leading - eigenvector are all the same, so the actual number of discovered - communities can be less than the desired one. - @param weights: name of an edge attribute or a list containing - edge weights. - @param arpack_options: an L{ARPACKOptions} object used to fine-tune - the ARPACK eigenvector calculation. If omitted, the module-level - variable called C{arpack_options} is used. - @return: an appropriate L{VertexClustering} object. - - @newfield ref: Reference - @ref: MEJ Newman: Finding community structure in networks using the - eigenvectors of matrices, arXiv:physics/0605087""" - if clusters is None: - clusters = -1 - - kwds = dict(weights=weights) - if arpack_options is not None: - kwds["arpack_options"] = arpack_options - - membership, _, q = GraphBase.community_leading_eigenvector( - self, clusters, **kwds - ) - return VertexClustering(self, membership, modularity=q) - - def community_label_propagation(self, weights=None, initial=None, fixed=None): - """Finds the community structure of the graph according to the label - propagation method of Raghavan et al. - - Initially, each vertex is assigned a different label. After that, - each vertex chooses the dominant label in its neighbourhood in each - iteration. Ties are broken randomly and the order in which the - vertices are updated is randomized before every iteration. The - algorithm ends when vertices reach a consensus. - Note that since ties are broken randomly, there is no guarantee that - the algorithm returns the same community structure after each run. - In fact, they frequently differ. See the paper of Raghavan et al - on how to come up with an aggregated community structure. - @param weights: name of an edge attribute or a list containing - edge weights - @param initial: name of a vertex attribute or a list containing - the initial vertex labels. Labels are identified by integers from - zero to M{n-1} where M{n} is the number of vertices. Negative - numbers may also be present in this vector, they represent unlabeled - vertices. - @param fixed: a list of booleans for each vertex. C{True} corresponds - to vertices whose labeling should not change during the algorithm. - It only makes sense if initial labels are also given. Unlabeled - vertices cannot be fixed. It may also be the name of a vertex - attribute; each attribute value will be interpreted as a Boolean. - @return: an appropriate L{VertexClustering} object. - - @newfield ref: Reference - @ref: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear - time algorithm to detect community structures in large-scale - networks. Phys Rev E 76:036106, 2007. - U{http://arxiv.org/abs/0709.2938}. - """ - if isinstance(fixed, str): - fixed = [bool(o) for o in self.vs[fixed]] - cl = GraphBase.community_label_propagation(self, weights, initial, fixed) - return VertexClustering(self, cl, modularity_params=dict(weights=weights)) - - def community_multilevel(self, weights=None, return_levels=False): - """Community structure based on the multilevel algorithm of - Blondel et al. - - This is a bottom-up algorithm: initially every vertex belongs to a - separate community, and vertices are moved between communities - iteratively in a way that maximizes the vertices' local contribution - to the overall modularity score. When a consensus is reached (i.e. no - single move would increase the modularity score), every community in - the original graph is shrank to a single vertex (while keeping the - total weight of the adjacent edges) and the process continues on the - next level. The algorithm stops when it is not possible to increase - the modularity any more after shrinking the communities to vertices. - - This algorithm is said to run almost in linear time on sparse graphs. - - @param weights: edge attribute name or a list containing edge - weights - @param return_levels: if C{True}, the communities at each level are - returned in a list. If C{False}, only the community structure with - the best modularity is returned. - @return: a list of L{VertexClustering} objects, one corresponding to - each level (if C{return_levels} is C{True}), or a L{VertexClustering} - corresponding to the best modularity. + community_fastgreedy = _community_fastgreedy - @newfield ref: Reference - @ref: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast - unfolding of community hierarchies in large networks, J Stat Mech - P10008 (2008), http://arxiv.org/abs/0803.0476 - """ - if self.is_directed(): - raise ValueError("input graph must be undirected") - - if return_levels: - levels, qs = GraphBase.community_multilevel(self, weights, True) - result = [] - for level, q in zip(levels, qs): - result.append( - VertexClustering( - self, level, q, modularity_params=dict(weights=weights) - ) - ) - else: - membership = GraphBase.community_multilevel(self, weights, False) - result = VertexClustering( - self, membership, modularity_params=dict(weights=weights) - ) - return result + community_infomap = _community_infomap - def community_optimal_modularity(self, *args, **kwds): - """Calculates the optimal modularity score of the graph and the - corresponding community structure. - - This function uses the GNU Linear Programming Kit to solve a large - integer optimization problem in order to find the optimal modularity - score and the corresponding community structure, therefore it is - unlikely to work for graphs larger than a few (less than a hundred) - vertices. Consider using one of the heuristic approaches instead if - you have such a large graph. - - @return: the calculated membership vector and the corresponding - modularity in a tuple.""" - membership, modularity = GraphBase.community_optimal_modularity( - self, *args, **kwds - ) - return VertexClustering(self, membership, modularity) - - def community_edge_betweenness(self, clusters=None, directed=True, weights=None): - """Community structure based on the betweenness of the edges in the - network. - - The idea is that the betweenness of the edges connecting two - communities is typically high, as many of the shortest paths between - nodes in separate communities go through them. So we gradually remove - the edge with the highest betweenness and recalculate the betweennesses - after every removal. This way sooner or later the network falls of to - separate components. The result of the clustering will be represented - by a dendrogram. - - @param clusters: the number of clusters we would like to see. This - practically defines the "level" where we "cut" the dendrogram to - get the membership vector of the vertices. If C{None}, the dendrogram - is cut at the level which maximizes the modularity when the graph is - unweighted; otherwise the dendrogram is cut at at a single cluster - (because cluster count selection based on modularities does not make - sense for this method if not all the weights are equal). - @param directed: whether the directionality of the edges should be - taken into account or not. - @param weights: name of an edge attribute or a list containing - edge weights. - @return: a L{VertexDendrogram} object, initally cut at the maximum - modularity or at the desired number of clusters. - """ - merges, qs = GraphBase.community_edge_betweenness(self, directed, weights) - if qs is not None: - qs.reverse() - if clusters is None: - if qs: - clusters = qs.index(max(qs)) + 1 - else: - clusters = 1 - return VertexDendrogram( - self, merges, clusters, modularity_params=dict(weights=weights) - ) + community_leading_eigenvector_naive = _community_leading_eigenvector_naive - def community_spinglass(self, *args, **kwds): - """Finds the community structure of the graph according to the - spinglass community detection method of Reichardt & Bornholdt. - - @param weights: edge weights to be used. Can be a sequence or - iterable or even an edge attribute name. - @param spins: integer, the number of spins to use. This is the - upper limit for the number of communities. It is not a problem - to supply a (reasonably) big number here, in which case some - spin states will be unpopulated. - @param parupdate: whether to update the spins of the vertices in - parallel (synchronously) or not - @param start_temp: the starting temperature - @param stop_temp: the stop temperature - @param cool_fact: cooling factor for the simulated annealing - @param update_rule: specifies the null model of the simulation. - Possible values are C{"config"} (a random graph with the same - vertex degrees as the input graph) or C{"simple"} (a random - graph with the same number of edges) - @param gamma: the gamma argument of the algorithm, specifying the - balance between the importance of present and missing edges - within a community. The default value of 1.0 assigns equal - importance to both of them. - @param implementation: currently igraph contains two implementations - of the spinglass community detection algorithm. The faster - original implementation is the default. The other implementation - is able to take into account negative weights, this can be - chosen by setting C{implementation} to C{"neg"} - @param lambda_: the lambda argument of the algorithm, which - specifies the balance between the importance of present and missing - negatively weighted edges within a community. Smaller values of - lambda lead to communities with less negative intra-connectivity. - If the argument is zero, the algorithm reduces to a graph coloring - algorithm, using the number of spins as colors. This argument is - ignored if the original implementation is used. Note the underscore - at the end of the argument name; this is due to the fact that - lambda is a reserved keyword in Python. - @return: an appropriate L{VertexClustering} object. + community_leading_eigenvector = _community_leading_eigenvector - @newfield ref: Reference - @ref: Reichardt J and Bornholdt S: Statistical mechanics of - community detection. Phys Rev E 74:016110 (2006). - U{http://arxiv.org/abs/cond-mat/0603718}. - @ref: Traag VA and Bruggeman J: Community detection in networks - with positive and negative links. Phys Rev E 80:036115 (2009). - U{http://arxiv.org/abs/0811.2329}. - """ - membership = GraphBase.community_spinglass(self, *args, **kwds) - if "weights" in kwds: - modularity_params = dict(weights=kwds["weights"]) - else: - modularity_params = {} - return VertexClustering(self, membership, modularity_params=modularity_params) + community_label_propagation = _community_label_propagation - def community_walktrap(self, weights=None, steps=4): - """Community detection algorithm of Latapy & Pons, based on random - walks. + community_multilevel = _community_multilevel - The basic idea of the algorithm is that short random walks tend to stay - in the same community. The result of the clustering will be represented - as a dendrogram. + community_optimal_modularity = _community_optimal_modularity - @param weights: name of an edge attribute or a list containing - edge weights - @param steps: length of random walks to perform + community_edge_betweenness = _community_edge_betweenness - @return: a L{VertexDendrogram} object, initially cut at the maximum - modularity. + community_spinglass = _community_spinglass - @newfield ref: Reference - @ref: Pascal Pons, Matthieu Latapy: Computing communities in large - networks using random walks, U{http://arxiv.org/abs/physics/0512106}. - """ - merges, qs = GraphBase.community_walktrap(self, weights, steps) - qs.reverse() - if qs: - optimal_count = qs.index(max(qs)) + 1 - else: - optimal_count = 1 - return VertexDendrogram( - self, merges, optimal_count, modularity_params=dict(weights=weights) - ) + community_walktrap = _community_walktrap - def k_core(self, *args): - """Returns some k-cores of the graph. + k_core = _k_core - The method accepts an arbitrary number of arguments representing - the desired indices of the M{k}-cores to be returned. The arguments - can also be lists or tuples. The result is a single L{Graph} object - if an only integer argument was given, otherwise the result is a - list of L{Graph} objects representing the desired k-cores in the - order the arguments were specified. If no argument is given, returns - all M{k}-cores in increasing order of M{k}. - """ - if len(args) == 0: - indices = range(self.vcount()) - return_single = False - else: - return_single = True - indices = [] - for arg in args: - try: - indices.extend(arg) - except Exception: - indices.append(arg) - - if len(indices) > 1 or hasattr(args[0], "__iter__"): - return_single = False - - corenesses = self.coreness() - result = [] - vidxs = range(self.vcount()) - for idx in indices: - core_idxs = [vidx for vidx in vidxs if corenesses[vidx] >= idx] - result.append(self.subgraph(core_idxs)) - - if return_single: - return result[0] - return result - - def community_leiden( - self, - objective_function="CPM", - weights=None, - resolution_parameter=1.0, - beta=0.01, - initial_membership=None, - n_iterations=2, - node_weights=None, - ): - """Finds the community structure of the graph using the Leiden - algorithm of Traag, van Eck & Waltman. - - @param objective_function: whether to use the Constant Potts - Model (CPM) or modularity. Must be either C{"CPM"} or C{"modularity"}. - @param weights: edge weights to be used. Can be a sequence or - iterable or even an edge attribute name. - @param resolution_parameter: the resolution parameter to use. - Higher resolutions lead to more smaller communities, while - lower resolutions lead to fewer larger communities. - @param beta: parameter affecting the randomness in the Leiden - algorithm. This affects only the refinement step of the algorithm. - @param initial_membership: if provided, the Leiden algorithm - will try to improve this provided membership. If no argument is - provided, the aglorithm simply starts from the singleton partition. - @param n_iterations: the number of iterations to iterate the Leiden - algorithm. Each iteration may improve the partition further. Using - a negative number of iterations will run until a stable iteration is - encountered (i.e. the quality was not increased during that - iteration). - @param node_weights: the node weights used in the Leiden algorithm. - If this is not provided, it will be automatically determined on the - basis of whether you want to use CPM or modularity. If you do provide - this, please make sure that you understand what you are doing. - @return: an appropriate L{VertexClustering} object. - - @newfield ref: Reference - @ref: Traag, V. A., Waltman, L., & van Eck, N. J. (2019). From Louvain - to Leiden: guaranteeing well-connected communities. Scientific - reports, 9(1), 5233. doi: 10.1038/s41598-019-41695-z - """ - if objective_function.lower() not in ("cpm", "modularity"): - raise ValueError('objective_function must be "CPM" or "modularity".') - - membership = GraphBase.community_leiden( - self, - edge_weights=weights, - node_weights=node_weights, - resolution_parameter=resolution_parameter, - normalize_resolution=(objective_function == "modularity"), - beta=beta, - initial_membership=initial_membership, - n_iterations=n_iterations, - ) - - if weights is not None: - modularity_params = dict(weights=weights) - else: - modularity_params = {} - return VertexClustering(self, membership, modularity_params=modularity_params) + community_leiden = _community_leiden def layout(self, layout=None, *args, **kwds): """Returns the layout of the graph according to a layout algorithm. @@ -3710,7 +3289,7 @@ def write(graph, filename, *args, **kwds): config = init_configuration() -# Remove constructors from namespace +# Remove modular methods from namespace del ( construct_graph_from_formula, _construct_graph_from_graphmlz_file, @@ -3739,4 +3318,16 @@ def write(graph, filename, *args, **kwds): _export_graph_to_dict_dict, _export_graph_to_dict_list, _export_graph_to_tuple_list, + _community_fastgreedy, + _community_infomap, + _community_leading_eigenvector_naive, + _community_leading_eigenvector, + _community_label_propagation, + _community_multilevel, + _community_optimal_modularity, + _community_edge_betweenness, + _community_spinglass, + _community_walktrap, + _k_core, + _community_leiden, ) diff --git a/src/igraph/community/__init__.py b/src/igraph/community/__init__.py new file mode 100644 index 000000000..cf5b56410 --- /dev/null +++ b/src/igraph/community/__init__.py @@ -0,0 +1,478 @@ +from igraph._igraph import GraphBase +from igraph.clustering import VertexDendrogram, VertexClustering +from igraph.utils import ( + safemax, +) + + +def _community_fastgreedy(graph, weights=None): + """Community structure based on the greedy optimization of modularity. + + This algorithm merges individual nodes into communities in a way that + greedily maximizes the modularity score of the graph. It can be proven + that if no merge can increase the current modularity score, the + algorithm can be stopped since no further increase can be achieved. + + This algorithm is said to run almost in linear time on sparse graphs. + + @param weights: edge attribute name or a list containing edge + weights + @return: an appropriate L{VertexDendrogram} object. + + @newfield ref: Reference + @ref: A Clauset, MEJ Newman and C Moore: Finding community structure + in very large networks. Phys Rev E 70, 066111 (2004). + """ + merges, qs = GraphBase.community_fastgreedy(graph, weights) + + # qs may be shorter than |V|-1 if we are left with a few separated + # communities in the end; take this into account + diff = graph.vcount() - len(qs) + qs.reverse() + if qs: + optimal_count = qs.index(max(qs)) + diff + 1 + else: + optimal_count = diff + + return VertexDendrogram( + graph, merges, optimal_count, modularity_params=dict(weights=weights) + ) + + +def _community_infomap(graph, edge_weights=None, vertex_weights=None, trials=10): + """Finds the community structure of the network according to the Infomap + method of Martin Rosvall and Carl T. Bergstrom. + + @param edge_weights: name of an edge attribute or a list containing + edge weights. + @param vertex_weights: name of an vertex attribute or a list containing + vertex weights. + @param trials: the number of attempts to partition the network. + @return: an appropriate L{VertexClustering} object with an extra attribute + called C{codelength} that stores the code length determined by the + algorithm. + + @newfield ref: Reference + @ref: M. Rosvall and C. T. Bergstrom: Maps of information flow reveal + community structure in complex networks, PNAS 105, 1118 (2008). + U{http://dx.doi.org/10.1073/pnas.0706851105}, + U{http://arxiv.org/abs/0707.0609}. + @ref: M. Rosvall, D. Axelsson, and C. T. Bergstrom: The map equation, + Eur. Phys. J. Special Topics 178, 13 (2009). + U{http://dx.doi.org/10.1140/epjst/e2010-01179-1}, + U{http://arxiv.org/abs/0906.1405}. + """ + membership, codelength = GraphBase.community_infomap( + graph, edge_weights, vertex_weights, trials + ) + return VertexClustering( + graph, + membership, + params={"codelength": codelength}, + modularity_params={"weights": edge_weights}, + ) + + +def _community_leading_eigenvector_naive(graph, clusters=None, return_merges=False): + """Naive implementation of Newman's eigenvector community structure detection. + + This function splits the network into two components + according to the leading eigenvector of the modularity matrix and + then recursively takes the given number of steps by splitting the + communities as individual networks. This is not the correct way, + however, see the reference for explanation. Consider using the + correct L{community_leading_eigenvector} method instead. + + @param clusters: the desired number of communities. If C{None}, the + algorithm tries to do as many splits as possible. Note that the + algorithm won't split a community further if the signs of the leading + eigenvector are all the same, so the actual number of discovered + communities can be less than the desired one. + @param return_merges: whether the returned object should be a + dendrogram instead of a single clustering. + @return: an appropriate L{VertexClustering} or L{VertexDendrogram} + object. + + @newfield ref: Reference + @ref: MEJ Newman: Finding community structure in networks using the + eigenvectors of matrices, arXiv:physics/0605087""" + if clusters is None: + clusters = -1 + cl, merges, q = GraphBase.community_leading_eigenvector_naive( + graph, clusters, return_merges + ) + if merges is None: + return VertexClustering(graph, cl, modularity=q) + else: + return VertexDendrogram(graph, merges, safemax(cl) + 1) + + +def _community_leading_eigenvector( + graph, clusters=None, weights=None, arpack_options=None +): + """Newman's leading eigenvector method for detecting community structure. + + This is the proper implementation of the recursive, divisive algorithm: + each split is done by maximizing the modularity regarding the + original network. + + @param clusters: the desired number of communities. If C{None}, the + algorithm tries to do as many splits as possible. Note that the + algorithm won't split a community further if the signs of the leading + eigenvector are all the same, so the actual number of discovered + communities can be less than the desired one. + @param weights: name of an edge attribute or a list containing + edge weights. + @param arpack_options: an L{ARPACKOptions} object used to fine-tune + the ARPACK eigenvector calculation. If omitted, the module-level + variable called C{arpack_options} is used. + @return: an appropriate L{VertexClustering} object. + + @newfield ref: Reference + @ref: MEJ Newman: Finding community structure in networks using the + eigenvectors of matrices, arXiv:physics/0605087""" + if clusters is None: + clusters = -1 + + kwds = dict(weights=weights) + if arpack_options is not None: + kwds["arpack_options"] = arpack_options + + membership, _, q = GraphBase.community_leading_eigenvector( + graph, clusters, **kwds + ) + return VertexClustering(graph, membership, modularity=q) + + +def _community_label_propagation(graph, weights=None, initial=None, fixed=None): + """Finds the community structure of the graph according to the label + propagation method of Raghavan et al. + + Initially, each vertex is assigned a different label. After that, + each vertex chooses the dominant label in its neighbourhood in each + iteration. Ties are broken randomly and the order in which the + vertices are updated is randomized before every iteration. The + algorithm ends when vertices reach a consensus. + Note that since ties are broken randomly, there is no guarantee that + the algorithm returns the same community structure after each run. + In fact, they frequently differ. See the paper of Raghavan et al + on how to come up with an aggregated community structure. + @param weights: name of an edge attribute or a list containing + edge weights + @param initial: name of a vertex attribute or a list containing + the initial vertex labels. Labels are identified by integers from + zero to M{n-1} where M{n} is the number of vertices. Negative + numbers may also be present in this vector, they represent unlabeled + vertices. + @param fixed: a list of booleans for each vertex. C{True} corresponds + to vertices whose labeling should not change during the algorithm. + It only makes sense if initial labels are also given. Unlabeled + vertices cannot be fixed. It may also be the name of a vertex + attribute; each attribute value will be interpreted as a Boolean. + @return: an appropriate L{VertexClustering} object. + + @newfield ref: Reference + @ref: Raghavan, U.N. and Albert, R. and Kumara, S. Near linear + time algorithm to detect community structures in large-scale + networks. Phys Rev E 76:036106, 2007. + U{http://arxiv.org/abs/0709.2938}. + """ + if isinstance(fixed, str): + fixed = [bool(o) for o in graph.vs[fixed]] + cl = GraphBase.community_label_propagation(graph, weights, initial, fixed) + return VertexClustering(graph, cl, modularity_params=dict(weights=weights)) + + +def _community_multilevel(graph, weights=None, return_levels=False): + """Community structure based on the multilevel algorithm of + Blondel et al. + + This is a bottom-up algorithm: initially every vertex belongs to a + separate community, and vertices are moved between communities + iteratively in a way that maximizes the vertices' local contribution + to the overall modularity score. When a consensus is reached (i.e. no + single move would increase the modularity score), every community in + the original graph is shrank to a single vertex (while keeping the + total weight of the adjacent edges) and the process continues on the + next level. The algorithm stops when it is not possible to increase + the modularity any more after shrinking the communities to vertices. + + This algorithm is said to run almost in linear time on sparse graphs. + + @param weights: edge attribute name or a list containing edge + weights + @param return_levels: if C{True}, the communities at each level are + returned in a list. If C{False}, only the community structure with + the best modularity is returned. + @return: a list of L{VertexClustering} objects, one corresponding to + each level (if C{return_levels} is C{True}), or a L{VertexClustering} + corresponding to the best modularity. + + @newfield ref: Reference + @ref: VD Blondel, J-L Guillaume, R Lambiotte and E Lefebvre: Fast + unfolding of community hierarchies in large networks, J Stat Mech + P10008 (2008), http://arxiv.org/abs/0803.0476 + """ + if graph.is_directed(): + raise ValueError("input graph must be undirected") + + if return_levels: + levels, qs = GraphBase.community_multilevel(graph, weights, True) + result = [] + for level, q in zip(levels, qs): + result.append( + VertexClustering( + graph, level, q, modularity_params=dict(weights=weights) + ) + ) + else: + membership = GraphBase.community_multilevel(graph, weights, False) + result = VertexClustering( + graph, membership, modularity_params=dict(weights=weights) + ) + return result + + +def _community_optimal_modularity(graph, *args, **kwds): + """Calculates the optimal modularity score of the graph and the + corresponding community structure. + + This function uses the GNU Linear Programming Kit to solve a large + integer optimization problem in order to find the optimal modularity + score and the corresponding community structure, therefore it is + unlikely to work for graphs larger than a few (less than a hundred) + vertices. Consider using one of the heuristic approaches instead if + you have such a large graph. + + @return: the calculated membership vector and the corresponding + modularity in a tuple.""" + membership, modularity = GraphBase.community_optimal_modularity( + graph, *args, **kwds + ) + return VertexClustering(graph, membership, modularity) + + +def _community_edge_betweenness(graph, clusters=None, directed=True, weights=None): + """Community structure based on the betweenness of the edges in the + network. + + The idea is that the betweenness of the edges connecting two + communities is typically high, as many of the shortest paths between + nodes in separate communities go through them. So we gradually remove + the edge with the highest betweenness and recalculate the betweennesses + after every removal. This way sooner or later the network falls of to + separate components. The result of the clustering will be represented + by a dendrogram. + + @param clusters: the number of clusters we would like to see. This + practically defines the "level" where we "cut" the dendrogram to + get the membership vector of the vertices. If C{None}, the dendrogram + is cut at the level which maximizes the modularity when the graph is + unweighted; otherwise the dendrogram is cut at at a single cluster + (because cluster count selection based on modularities does not make + sense for this method if not all the weights are equal). + @param directed: whether the directionality of the edges should be + taken into account or not. + @param weights: name of an edge attribute or a list containing + edge weights. + @return: a L{VertexDendrogram} object, initally cut at the maximum + modularity or at the desired number of clusters. + """ + merges, qs = GraphBase.community_edge_betweenness(graph, directed, weights) + if qs is not None: + qs.reverse() + if clusters is None: + if qs: + clusters = qs.index(max(qs)) + 1 + else: + clusters = 1 + return VertexDendrogram( + graph, merges, clusters, modularity_params=dict(weights=weights) + ) + + +def _community_spinglass(graph, *args, **kwds): + """Finds the community structure of the graph according to the + spinglass community detection method of Reichardt & Bornholdt. + + @param weights: edge weights to be used. Can be a sequence or + iterable or even an edge attribute name. + @param spins: integer, the number of spins to use. This is the + upper limit for the number of communities. It is not a problem + to supply a (reasonably) big number here, in which case some + spin states will be unpopulated. + @param parupdate: whether to update the spins of the vertices in + parallel (synchronously) or not + @param start_temp: the starting temperature + @param stop_temp: the stop temperature + @param cool_fact: cooling factor for the simulated annealing + @param update_rule: specifies the null model of the simulation. + Possible values are C{"config"} (a random graph with the same + vertex degrees as the input graph) or C{"simple"} (a random + graph with the same number of edges) + @param gamma: the gamma argument of the algorithm, specifying the + balance between the importance of present and missing edges + within a community. The default value of 1.0 assigns equal + importance to both of them. + @param implementation: currently igraph contains two implementations + of the spinglass community detection algorithm. The faster + original implementation is the default. The other implementation + is able to take into account negative weights, this can be + chosen by setting C{implementation} to C{"neg"} + @param lambda_: the lambda argument of the algorithm, which + specifies the balance between the importance of present and missing + negatively weighted edges within a community. Smaller values of + lambda lead to communities with less negative intra-connectivity. + If the argument is zero, the algorithm reduces to a graph coloring + algorithm, using the number of spins as colors. This argument is + ignored if the original implementation is used. Note the underscore + at the end of the argument name; this is due to the fact that + lambda is a reserved keyword in Python. + @return: an appropriate L{VertexClustering} object. + + @newfield ref: Reference + @ref: Reichardt J and Bornholdt S: Statistical mechanics of + community detection. Phys Rev E 74:016110 (2006). + U{http://arxiv.org/abs/cond-mat/0603718}. + @ref: Traag VA and Bruggeman J: Community detection in networks + with positive and negative links. Phys Rev E 80:036115 (2009). + U{http://arxiv.org/abs/0811.2329}. + """ + membership = GraphBase.community_spinglass(graph, *args, **kwds) + if "weights" in kwds: + modularity_params = dict(weights=kwds["weights"]) + else: + modularity_params = {} + return VertexClustering(graph, membership, modularity_params=modularity_params) + + +def _community_walktrap(graph, weights=None, steps=4): + """Community detection algorithm of Latapy & Pons, based on random + walks. + + The basic idea of the algorithm is that short random walks tend to stay + in the same community. The result of the clustering will be represented + as a dendrogram. + + @param weights: name of an edge attribute or a list containing + edge weights + @param steps: length of random walks to perform + + @return: a L{VertexDendrogram} object, initially cut at the maximum + modularity. + + @newfield ref: Reference + @ref: Pascal Pons, Matthieu Latapy: Computing communities in large + networks using random walks, U{http://arxiv.org/abs/physics/0512106}. + """ + merges, qs = GraphBase.community_walktrap(graph, weights, steps) + qs.reverse() + if qs: + optimal_count = qs.index(max(qs)) + 1 + else: + optimal_count = 1 + return VertexDendrogram( + graph, merges, optimal_count, modularity_params=dict(weights=weights) + ) + + +def _k_core(graph, *args): + """Returns some k-cores of the graph. + + The method accepts an arbitrary number of arguments representing + the desired indices of the M{k}-cores to be returned. The arguments + can also be lists or tuples. The result is a single L{Graph} object + if an only integer argument was given, otherwise the result is a + list of L{Graph} objects representing the desired k-cores in the + order the arguments were specified. If no argument is given, returns + all M{k}-cores in increasing order of M{k}. + """ + if len(args) == 0: + indices = range(graph.vcount()) + return_single = False + else: + return_single = True + indices = [] + for arg in args: + try: + indices.extend(arg) + except Exception: + indices.append(arg) + + if len(indices) > 1 or hasattr(args[0], "__iter__"): + return_single = False + + corenesses = graph.coreness() + result = [] + vidxs = range(graph.vcount()) + for idx in indices: + core_idxs = [vidx for vidx in vidxs if corenesses[vidx] >= idx] + result.append(graph.subgraph(core_idxs)) + + if return_single: + return result[0] + return result + + +def _community_leiden( + graph, + objective_function="CPM", + weights=None, + resolution_parameter=1.0, + beta=0.01, + initial_membership=None, + n_iterations=2, + node_weights=None, +): + """Finds the community structure of the graph using the Leiden + algorithm of Traag, van Eck & Waltman. + + @param objective_function: whether to use the Constant Potts + Model (CPM) or modularity. Must be either C{"CPM"} or C{"modularity"}. + @param weights: edge weights to be used. Can be a sequence or + iterable or even an edge attribute name. + @param resolution_parameter: the resolution parameter to use. + Higher resolutions lead to more smaller communities, while + lower resolutions lead to fewer larger communities. + @param beta: parameter affecting the randomness in the Leiden + algorithm. This affects only the refinement step of the algorithm. + @param initial_membership: if provided, the Leiden algorithm + will try to improve this provided membership. If no argument is + provided, the aglorithm simply starts from the singleton partition. + @param n_iterations: the number of iterations to iterate the Leiden + algorithm. Each iteration may improve the partition further. Using + a negative number of iterations will run until a stable iteration is + encountered (i.e. the quality was not increased during that + iteration). + @param node_weights: the node weights used in the Leiden algorithm. + If this is not provided, it will be automatically determined on the + basis of whether you want to use CPM or modularity. If you do provide + this, please make sure that you understand what you are doing. + @return: an appropriate L{VertexClustering} object. + + @newfield ref: Reference + @ref: Traag, V. A., Waltman, L., & van Eck, N. J. (2019). From Louvain + to Leiden: guaranteeing well-connected communities. Scientific + reports, 9(1), 5233. doi: 10.1038/s41598-019-41695-z + """ + if objective_function.lower() not in ("cpm", "modularity"): + raise ValueError('objective_function must be "CPM" or "modularity".') + + membership = GraphBase.community_leiden( + graph, + edge_weights=weights, + node_weights=node_weights, + resolution_parameter=resolution_parameter, + normalize_resolution=(objective_function == "modularity"), + beta=beta, + initial_membership=initial_membership, + n_iterations=n_iterations, + ) + + if weights is not None: + modularity_params = dict(weights=weights) + else: + modularity_params = {} + return VertexClustering(graph, membership, modularity_params=modularity_params) + + From dff42bf6566d0e59d70ca3579cf20dc22cda4ae6 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 2 Oct 2021 15:37:31 +1000 Subject: [PATCH 0468/1681] Vertex/EdgeSeq, __plot__, operators --- src/igraph/__init__.py | 1146 +---------------- src/igraph/drawing/graph.py | 202 ++- src/igraph/drawing/utils.py | 65 + src/igraph/operators/__init__.py | 35 + .../{operators.py => operators/functions.py} | 0 src/igraph/operators/methods.py | 211 +++ src/igraph/seq/__init__.py | 700 ++++++++++ src/igraph/seq/edgeseq.py | 431 +++++++ 8 files changed, 1662 insertions(+), 1128 deletions(-) create mode 100644 src/igraph/operators/__init__.py rename src/igraph/{operators.py => operators/functions.py} (100%) create mode 100644 src/igraph/operators/methods.py create mode 100644 src/igraph/seq/__init__.py create mode 100644 src/igraph/seq/edgeseq.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 7a53fd765..974822571 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -132,6 +132,8 @@ palettes, known_colors, ) +from igraph.drawing.graph import __plot__ as _graph_plot +from igraph.drawing.utils import autocurve from igraph.datatypes import Matrix, DyadCensus, TriadCensus, UniqueIdGenerator from igraph.formula import construct_graph_from_formula from igraph.io.files import ( @@ -183,7 +185,13 @@ from igraph.io.images import _write_graph_to_svg from igraph.layout import Layout from igraph.matching import Matching -from igraph.operators import disjoint_union, union, intersection +from igraph.operators import ( + disjoint_union, + union, + intersection, + operator_method_registry as _operator_method_registry, +) +from igraph.seq import EdgeSeq, VertexSeq from igraph.statistics import ( FittedPowerLaw, Histogram, @@ -1736,184 +1744,24 @@ def _as_parameter_(self): ################### # Custom operators + __iadd__ = _operator_method_registry['__iadd__'] - def __iadd__(self, other): - """In-place addition (disjoint union). - - @see: L{__add__} - """ - if isinstance(other, (int, str)): - self.add_vertices(other) - return self - elif isinstance(other, tuple) and len(other) == 2: - self.add_edges([other]) - return self - elif isinstance(other, list): - if not other: - return self - if isinstance(other[0], tuple): - self.add_edges(other) - return self - if isinstance(other[0], str): - self.add_vertices(other) - return self - return NotImplemented - - def __add__(self, other): - """Copies the graph and extends the copy depending on the type of - the other object given. - - @param other: if it is an integer, the copy is extended by the given - number of vertices. If it is a string, the copy is extended by a - single vertex whose C{name} attribute will be equal to the given - string. If it is a tuple with two elements, the copy - is extended by a single edge. If it is a list of tuples, the copy - is extended by multiple edges. If it is a L{Graph}, a disjoint - union is performed. - """ - if isinstance(other, (int, str)): - g = self.copy() - g.add_vertices(other) - elif isinstance(other, tuple) and len(other) == 2: - g = self.copy() - g.add_edges([other]) - elif isinstance(other, list): - if len(other) > 0: - if isinstance(other[0], tuple): - g = self.copy() - g.add_edges(other) - elif isinstance(other[0], str): - g = self.copy() - g.add_vertices(other) - elif isinstance(other[0], Graph): - return self.disjoint_union(other) - else: - return NotImplemented - else: - return self.copy() - - elif isinstance(other, Graph): - return self.disjoint_union(other) - else: - return NotImplemented + __add__ = _operator_method_registry['__add__'] - return g + __and__ = _operator_method_registry['__and__'] - def __and__(self, other): - """Graph intersection operator. + __isub__ = _operator_method_registry['__isub__'] - @param other: the other graph to take the intersection with. - @return: the intersected graph. - """ - if isinstance(other, Graph): - return self.intersection(other) - else: - return NotImplemented - - def __isub__(self, other): - """In-place subtraction (difference). - - @see: L{__sub__}""" - if isinstance(other, int): - self.delete_vertices([other]) - elif isinstance(other, tuple) and len(other) == 2: - self.delete_edges([other]) - elif isinstance(other, list): - if len(other) > 0: - if isinstance(other[0], tuple): - self.delete_edges(other) - elif isinstance(other[0], (int, str)): - self.delete_vertices(other) - else: - return NotImplemented - elif isinstance(other, Vertex): - self.delete_vertices(other) - elif isinstance(other, VertexSeq): - self.delete_vertices(other) - elif isinstance(other, Edge): - self.delete_edges(other) - elif isinstance(other, EdgeSeq): - self.delete_edges(other) - else: - return NotImplemented - return self - - def __sub__(self, other): - """Removes the given object(s) from the graph - - @param other: if it is an integer, removes the vertex with the given - ID from the graph (note that the remaining vertices will get - re-indexed!). If it is a tuple, removes the given edge. If it is - a graph, takes the difference of the two graphs. Accepts - lists of integers or lists of tuples as well, but they can't be - mixed! Also accepts L{Edge} and L{EdgeSeq} objects. - """ - if isinstance(other, Graph): - return self.difference(other) - - result = self.copy() - if isinstance(other, (int, str)): - result.delete_vertices([other]) - elif isinstance(other, tuple) and len(other) == 2: - result.delete_edges([other]) - elif isinstance(other, list): - if len(other) > 0: - if isinstance(other[0], tuple): - result.delete_edges(other) - elif isinstance(other[0], (int, str)): - result.delete_vertices(other) - else: - return NotImplemented - else: - return result - elif isinstance(other, Vertex): - result.delete_vertices(other) - elif isinstance(other, VertexSeq): - result.delete_vertices(other) - elif isinstance(other, Edge): - result.delete_edges(other) - elif isinstance(other, EdgeSeq): - result.delete_edges(other) - else: - return NotImplemented + __sub__ = _operator_method_registry['__sub__'] - return result + __mul__ = _operator_method_registry['__mul__'] - def __mul__(self, other): - """Copies exact replicas of the original graph an arbitrary number of - times. - - @param other: if it is an integer, multiplies the graph by creating the - given number of identical copies and taking the disjoint union of - them. - """ - if isinstance(other, int): - if other == 0: - return Graph() - elif other == 1: - return self - elif other > 1: - return self.disjoint_union([self] * (other - 1)) - else: - return NotImplemented - - return NotImplemented + __or__ = _operator_method_registry['__or__'] def __bool__(self): """Returns True if the graph has at least one vertex, False otherwise.""" return self.vcount() > 0 - def __or__(self, other): - """Graph union operator. - - @param other: the other graph to take the union with. - @return: the union graph. - """ - if isinstance(other, Graph): - return self.union(other) - else: - return NotImplemented - def __coerce__(self, other): """Coercion rules. @@ -1965,204 +1813,7 @@ def __reduce__(self): __iter__ = None # needed for PyPy __hash__ = None # needed for PyPy - def __plot__(self, backend, context, *args, **kwds): - """Plots the graph to the given Cairo context or matplotlib Axes. - - The visual style of vertices and edges can be modified at three - places in the following order of precedence (lower indices override - higher indices): - - 1. Keyword arguments of this function (or of L{plot()} which is - passed intact to C{Graph.__plot__()}. - - 2. Vertex or edge attributes, specified later in the list of - keyword arguments. - - 3. igraph-wide plotting defaults (see - L{igraph.config.Configuration}) - - 4. Built-in defaults. - - E.g., if the C{vertex_size} keyword attribute is not present, - but there exists a vertex attribute named C{size}, the sizes of - the vertices will be specified by that attribute. - - Besides the usual self-explanatory plotting parameters (C{context}, - C{bbox}, C{palette}), it accepts the following keyword arguments: - - - C{autocurve}: whether to use curves instead of straight lines for - multiple edges on the graph plot. This argument may be C{True} - or C{False}; when omitted, C{True} is assumed for graphs with - less than 10.000 edges and C{False} otherwise. - - - C{drawer_factory}: a subclass of L{AbstractCairoGraphDrawer} - which will be used to draw the graph. You may also provide - a function here which takes two arguments: the Cairo context - to draw on and a bounding box (an instance of L{BoundingBox}). - If this keyword argument is missing, igraph will use the - default graph drawer which should be suitable for most purposes. - It is safe to omit this keyword argument unless you need to use - a specific graph drawer. - - - C{keep_aspect_ratio}: whether to keep the aspect ratio of the layout - that igraph calculates to place the nodes. C{True} means that the - layout will be scaled proportionally to fit into the bounding box - where the graph is to be drawn but the aspect ratio will be kept - the same (potentially leaving empty space next to, below or above - the graph). C{False} means that the layout will be scaled independently - along the X and Y axis in order to fill the entire bounding box. - The default is C{False}. - - - C{layout}: the layout to be used. If not an instance of - L{Layout}, it will be passed to L{layout} to calculate - the layout. Note that if you want a deterministic layout that - does not change with every plot, you must either use a - deterministic layout function (like L{layout_circle}) or - calculate the layout in advance and pass a L{Layout} object here. - - - C{margin}: the top, right, bottom, left margins as a 4-tuple. - If it has less than 4 elements or is a single float, the elements - will be re-used until the length is at least 4. - - - C{mark_groups}: whether to highlight some of the vertex groups by - colored polygons. This argument can be one of the following: - - - C{False}: no groups will be highlighted - - - C{True}: only valid if the object plotted is a - L{VertexClustering} or L{VertexCover}. The vertex groups in the - clutering or cover will be highlighted such that the i-th - group will be colored by the i-th color from the current - palette. If used when plotting a graph, it will throw an error. - - - A dict mapping tuples of vertex indices to color names. - The given vertex groups will be highlighted by the given - colors. - - - A list containing pairs or an iterable yielding pairs, where - the first element of each pair is a list of vertex indices and - the second element is a color. - - - A L{VertexClustering} or L{VertexCover} instance. The vertex - groups in the clustering or cover will be highlighted such that - the i-th group will be colored by the i-th color from the - current palette. - - In place of lists of vertex indices, you may also use L{VertexSeq} - instances. - - In place of color names, you may also use color indices into the - current palette. C{None} as a color name will mean that the - corresponding group is ignored. - - - C{vertex_size}: size of the vertices. The corresponding vertex - attribute is called C{size}. The default is 10. Vertex sizes - are measured in the unit of the Cairo context on which igraph - is drawing. - - - C{vertex_color}: color of the vertices. The corresponding vertex - attribute is C{color}, the default is red. Colors can be - specified either by common X11 color names (see the source - code of L{igraph.drawing.colors} for a list of known colors), by - 3-tuples of floats (ranging between 0 and 255 for the R, G and - B components), by CSS-style string specifications (C{#rrggbb}) - or by integer color indices of the specified palette. - - - C{vertex_frame_color}: color of the frame (i.e. stroke) of the - vertices. The corresponding vertex attribute is C{frame_color}, - the default is black. See C{vertex_color} for the possible ways - of specifying a color. - - - C{vertex_frame_width}: the width of the frame (i.e. stroke) of the - vertices. The corresponding vertex attribute is C{frame_width}. - The default is 1. Vertex frame widths are measured in the unit of the - Cairo context on which igraph is drawing. - - - C{vertex_shape}: shape of the vertices. Alternatively it can - be specified by the C{shape} vertex attribute. Possibilities - are: C{square}, {circle}, {triangle}, {triangle-down} or - C{hidden}. See the source code of L{igraph.drawing} for a - list of alternative shape names that are also accepted and - mapped to these. - - - C{vertex_label}: labels drawn next to the vertices. - The corresponding vertex attribute is C{label}. - - - C{vertex_label_dist}: distance of the midpoint of the vertex - label from the center of the corresponding vertex. - The corresponding vertex attribute is C{label_dist}. - - - C{vertex_label_color}: color of the label. Corresponding - vertex attribute: C{label_color}. See C{vertex_color} for - color specification syntax. - - - C{vertex_label_size}: font size of the label, specified - in the unit of the Cairo context on which we are drawing. - Corresponding vertex attribute: C{label_size}. - - - C{vertex_label_angle}: the direction of the line connecting - the midpoint of the vertex with the midpoint of the label. - This can be used to position the labels relative to the - vertices themselves in conjunction with C{vertex_label_dist}. - Corresponding vertex attribute: C{label_angle}. The - default is C{-math.pi/2}. - - - C{vertex_order}: drawing order of the vertices. This must be - a list or tuple containing vertex indices; vertices are then - drawn according to this order. - - - C{vertex_order_by}: an alternative way to specify the drawing - order of the vertices; this attribute is interpreted as the name - of a vertex attribute, and vertices are drawn such that those - with a smaller attribute value are drawn first. You may also - reverse the order by passing a tuple here; the first element of - the tuple should be the name of the attribute, the second element - specifies whether the order is reversed (C{True}, C{False}, - C{"asc"} and C{"desc"} are accepted values). - - - C{edge_color}: color of the edges. The corresponding edge - attribute is C{color}, the default is red. See C{vertex_color} - for color specification syntax. - - - C{edge_curved}: whether the edges should be curved. Positive - numbers correspond to edges curved in a counter-clockwise - direction, negative numbers correspond to edges curved in a - clockwise direction. Zero represents straight edges. C{True} - is interpreted as 0.5, C{False} is interpreted as 0. The - default is 0 which makes all the edges straight. - - - C{edge_width}: width of the edges in the default unit of the - Cairo context on which we are drawing. The corresponding - edge attribute is C{width}, the default is 1. - - - C{edge_arrow_size}: arrow size of the edges. The - corresponding edge attribute is C{arrow_size}, the default - is 1. - - - C{edge_arrow_width}: width of the arrowhead on the edge. The - corresponding edge attribute is C{arrow_width}, the default - is 1. - - - C{edge_order}: drawing order of the edges. This must be - a list or tuple containing edge indices; edges are then - drawn according to this order. - - - C{edge_order_by}: an alternative way to specify the drawing - order of the edges; this attribute is interpreted as the name - of an edge attribute, and edges are drawn such that those - with a smaller attribute value are drawn first. You may also - reverse the order by passing a tuple here; the first element of - the tuple should be the name of the attribute, the second element - specifies whether the order is reversed (C{True}, C{False}, - C{"asc"} and C{"desc"} are accepted values). - """ - from igraph.drawing import DrawerDirectory - - drawer = kwds.pop( - "drawer_factory", - DrawerDirectory.resolve(self, backend)(context), - ) - drawer.draw(self, *args, **kwds) + __plot__ = _graph_plot def __str__(self): """Returns a string representation of the graph. @@ -2299,704 +1950,6 @@ def intersection(self, other, byname="auto"): # of Graph.layout if necessary! -############################################################## - - -class VertexSeq(_VertexSeq): - """Class representing a sequence of vertices in the graph. - - This class is most easily accessed by the C{vs} field of the - L{Graph} object, which returns an ordered sequence of all vertices in - the graph. The vertex sequence can be refined by invoking the - L{VertexSeq.select()} method. L{VertexSeq.select()} can also be - accessed by simply calling the L{VertexSeq} object. - - An alternative way to create a vertex sequence referring to a given - graph is to use the constructor directly: - - >>> g = Graph.Full(3) - >>> vs = VertexSeq(g) - >>> restricted_vs = VertexSeq(g, [0, 1]) - - The individual vertices can be accessed by indexing the vertex sequence - object. It can be used as an iterable as well, or even in a list - comprehension: - - >>> g=Graph.Full(3) - >>> for v in g.vs: - ... v["value"] = v.index ** 2 - ... - >>> [v["value"] ** 0.5 for v in g.vs] - [0.0, 1.0, 2.0] - - The vertex set can also be used as a dictionary where the keys are the - attribute names. The values corresponding to the keys are the values - of the given attribute for every vertex selected by the sequence. - - >>> g=Graph.Full(3) - >>> for idx, v in enumerate(g.vs): - ... v["weight"] = idx*(idx+1) - ... - >>> g.vs["weight"] - [0, 2, 6] - >>> g.vs.select(1,2)["weight"] = [10, 20] - >>> g.vs["weight"] - [0, 10, 20] - - If you specify a sequence that is shorter than the number of vertices in - the VertexSeq, the sequence is reused: - - >>> g = Graph.Tree(7, 2) - >>> g.vs["color"] = ["red", "green"] - >>> g.vs["color"] - ['red', 'green', 'red', 'green', 'red', 'green', 'red'] - - You can even pass a single string or integer, it will be considered as a - sequence of length 1: - - >>> g.vs["color"] = "red" - >>> g.vs["color"] - ['red', 'red', 'red', 'red', 'red', 'red', 'red'] - - Some methods of the vertex sequences are simply proxy methods to the - corresponding methods in the L{Graph} object. One such example is - C{VertexSeq.degree()}: - - >>> g=Graph.Tree(7, 2) - >>> g.vs.degree() - [2, 3, 3, 1, 1, 1, 1] - >>> g.vs.degree() == g.degree() - True - """ - - def attributes(self): - """Returns the list of all the vertex attributes in the graph - associated to this vertex sequence.""" - return self.graph.vertex_attributes() - - def find(self, *args, **kwds): - """Returns the first vertex of the vertex sequence that matches some - criteria. - - The selection criteria are equal to the ones allowed by L{VertexSeq.select}. - See L{VertexSeq.select} for more details. - - For instance, to find the first vertex with name C{foo} in graph C{g}: - - >>> g.vs.find(name="foo") #doctest:+SKIP - - To find an arbitrary isolated vertex: - - >>> g.vs.find(_degree=0) #doctest:+SKIP - """ - # Shortcut: if "name" is in kwds, there are no positional arguments, - # and the specified name is a string, we try that first because that - # attribute is indexed. Note that we cannot do this if name is an - # integer, because it would then translate to g.vs.select(name), which - # searches by _index_ if the argument is an integer - if not args: - if "name" in kwds: - name = kwds.pop("name") - elif "name_eq" in kwds: - name = kwds.pop("name_eq") - else: - name = None - - if name is not None: - if isinstance(name, str): - args = [name] - else: - # put back what we popped - kwds["name"] = name - - if args: - # Selecting first based on positional arguments, then checking - # the criteria specified by the (remaining) keyword arguments - vertex = _VertexSeq.find(self, *args) - if not kwds: - return vertex - vs = self.graph.vs.select(vertex.index) - else: - vs = self - - # Selecting based on keyword arguments - vs = vs.select(**kwds) - if vs: - return vs[0] - raise ValueError("no such vertex") - - def select(self, *args, **kwds): - """Selects a subset of the vertex sequence based on some criteria - - The selection criteria can be specified by the positional and the keyword - arguments. Positional arguments are always processed before keyword - arguments. - - - If the first positional argument is C{None}, an empty sequence is - returned. - - - If the first positional argument is a callable object, the object - will be called for every vertex in the sequence. If it returns - C{True}, the vertex will be included, otherwise it will - be excluded. - - - If the first positional argument is an iterable, it must return - integers and they will be considered as indices of the current - vertex set (NOT the whole vertex set of the graph -- the - difference matters when one filters a vertex set that has - already been filtered by a previous invocation of - L{VertexSeq.select()}. In this case, the indices do not refer - directly to the vertices of the graph but to the elements of - the filtered vertex sequence. - - - If the first positional argument is an integer, all remaining - arguments are expected to be integers. They are considered as - indices of the current vertex set again. - - Keyword arguments can be used to filter the vertices based on their - attributes. The name of the keyword specifies the name of the attribute - and the filtering operator, they should be concatenated by an - underscore (C{_}) character. Attribute names can also contain - underscores, but operator names don't, so the operator is always the - largest trailing substring of the keyword name that does not contain - an underscore. Possible operators are: - - - C{eq}: equal to - - - C{ne}: not equal to - - - C{lt}: less than - - - C{gt}: greater than - - - C{le}: less than or equal to - - - C{ge}: greater than or equal to - - - C{in}: checks if the value of an attribute is in a given list - - - C{notin}: checks if the value of an attribute is not in a given - list - - For instance, if you want to filter vertices with a numeric C{age} - property larger than 200, you have to write: - - >>> g.vs.select(age_gt=200) #doctest: +SKIP - - Similarly, to filter vertices whose C{type} is in a list of predefined - types: - - >>> list_of_types = ["HR", "Finance", "Management"] - >>> g.vs.select(type_in=list_of_types) #doctest: +SKIP - - If the operator is omitted, it defaults to C{eq}. For instance, the - following selector selects vertices whose C{cluster} property equals - to 2: - - >>> g.vs.select(cluster=2) #doctest: +SKIP - - In the case of an unknown operator, it is assumed that the - recognized operator is part of the attribute name and the actual - operator is C{eq}. - - Attribute names inferred from keyword arguments are treated specially - if they start with an underscore (C{_}). These are not real attributes - but refer to specific properties of the vertices, e.g., its degree. - The rule is as follows: if an attribute name starts with an underscore, - the rest of the name is interpreted as a method of the L{Graph} object. - This method is called with the vertex sequence as its first argument - (all others left at default values) and vertices are filtered - according to the value returned by the method. For instance, if you - want to exclude isolated vertices: - - >>> g = Graph.Famous("zachary") - >>> non_isolated = g.vs.select(_degree_gt=0) - - For properties that take a long time to be computed (e.g., betweenness - centrality for large graphs), it is advised to calculate the values - in advance and store it in a graph attribute. The same applies when - you are selecting based on the same property more than once in the - same C{select()} call to avoid calculating it twice unnecessarily. - For instance, the following would calculate betweenness centralities - twice: - - >>> edges = g.vs.select(_betweenness_gt=10, _betweenness_lt=30) - - It is advised to use this instead: - - >>> g.vs["bs"] = g.betweenness() - >>> edges = g.vs.select(bs_gt=10, bs_lt=30) - - @return: the new, filtered vertex sequence""" - vs = _VertexSeq.select(self, *args) - - operators = { - "lt": operator.lt, - "gt": operator.gt, - "le": operator.le, - "ge": operator.ge, - "eq": operator.eq, - "ne": operator.ne, - "in": lambda a, b: a in b, - "notin": lambda a, b: a not in b, - } - for keyword, value in kwds.items(): - if "_" not in keyword or keyword.rindex("_") == 0: - keyword = keyword + "_eq" - attr, _, op = keyword.rpartition("_") - try: - func = operators[op] - except KeyError: - # No such operator, assume that it's part of the attribute name - attr, op, func = keyword, "eq", operators["eq"] - - if attr[0] == "_": - # Method call, not an attribute - values = getattr(vs.graph, attr[1:])(vs) - else: - values = vs[attr] - filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] - vs = vs.select(filtered_idxs) - - return vs - - def __call__(self, *args, **kwds): - """Shorthand notation to select() - - This method simply passes all its arguments to L{VertexSeq.select()}. - """ - return self.select(*args, **kwds) - - -############################################################## - - -class EdgeSeq(_EdgeSeq): - """Class representing a sequence of edges in the graph. - - This class is most easily accessed by the C{es} field of the - L{Graph} object, which returns an ordered sequence of all edges in - the graph. The edge sequence can be refined by invoking the - L{EdgeSeq.select()} method. L{EdgeSeq.select()} can also be - accessed by simply calling the L{EdgeSeq} object. - - An alternative way to create an edge sequence referring to a given - graph is to use the constructor directly: - - >>> g = Graph.Full(3) - >>> es = EdgeSeq(g) - >>> restricted_es = EdgeSeq(g, [0, 1]) - - The individual edges can be accessed by indexing the edge sequence - object. It can be used as an iterable as well, or even in a list - comprehension: - - >>> g=Graph.Full(3) - >>> for e in g.es: - ... print(e.tuple) - ... - (0, 1) - (0, 2) - (1, 2) - >>> [max(e.tuple) for e in g.es] - [1, 2, 2] - - The edge sequence can also be used as a dictionary where the keys are the - attribute names. The values corresponding to the keys are the values - of the given attribute of every edge in the graph: - - >>> g=Graph.Full(3) - >>> for idx, e in enumerate(g.es): - ... e["weight"] = idx*(idx+1) - ... - >>> g.es["weight"] - [0, 2, 6] - >>> g.es["weight"] = range(3) - >>> g.es["weight"] - [0, 1, 2] - - If you specify a sequence that is shorter than the number of edges in - the EdgeSeq, the sequence is reused: - - >>> g = Graph.Tree(7, 2) - >>> g.es["color"] = ["red", "green"] - >>> g.es["color"] - ['red', 'green', 'red', 'green', 'red', 'green'] - - You can even pass a single string or integer, it will be considered as a - sequence of length 1: - - >>> g.es["color"] = "red" - >>> g.es["color"] - ['red', 'red', 'red', 'red', 'red', 'red'] - - Some methods of the edge sequences are simply proxy methods to the - corresponding methods in the L{Graph} object. One such example is - C{EdgeSeq.is_multiple()}: - - >>> g=Graph(3, [(0,1), (1,0), (1,2)]) - >>> g.es.is_multiple() - [False, True, False] - >>> g.es.is_multiple() == g.is_multiple() - True - """ - - def attributes(self): - """Returns the list of all the edge attributes in the graph - associated to this edge sequence.""" - return self.graph.edge_attributes() - - def find(self, *args, **kwds): - """Returns the first edge of the edge sequence that matches some - criteria. - - The selection criteria are equal to the ones allowed by L{VertexSeq.select}. - See L{VertexSeq.select} for more details. - - For instance, to find the first edge with weight larger than 5 in graph C{g}: - - >>> g.es.find(weight_gt=5) #doctest:+SKIP - """ - if args: - # Selecting first based on positional arguments, then checking - # the criteria specified by the keyword arguments - edge = _EdgeSeq.find(self, *args) - if not kwds: - return edge - es = self.graph.es.select(edge.index) - else: - es = self - - # Selecting based on positional arguments - es = es.select(**kwds) - if es: - return es[0] - raise ValueError("no such edge") - - def select(self, *args, **kwds): - """Selects a subset of the edge sequence based on some criteria - - The selection criteria can be specified by the positional and the - keyword arguments. Positional arguments are always processed before - keyword arguments. - - - If the first positional argument is C{None}, an empty sequence is - returned. - - - If the first positional argument is a callable object, the object - will be called for every edge in the sequence. If it returns - C{True}, the edge will be included, otherwise it will - be excluded. - - - If the first positional argument is an iterable, it must return - integers and they will be considered as indices of the current - edge set (NOT the whole edge set of the graph -- the - difference matters when one filters an edge set that has - already been filtered by a previous invocation of - L{EdgeSeq.select()}. In this case, the indices do not refer - directly to the edges of the graph but to the elements of - the filtered edge sequence. - - - If the first positional argument is an integer, all remaining - arguments are expected to be integers. They are considered as - indices of the current edge set again. - - Keyword arguments can be used to filter the edges based on their - attributes and properties. The name of the keyword specifies the name - of the attribute and the filtering operator, they should be - concatenated by an underscore (C{_}) character. Attribute names can - also contain underscores, but operator names don't, so the operator is - always the largest trailing substring of the keyword name that does not - contain an underscore. Possible operators are: - - - C{eq}: equal to - - - C{ne}: not equal to - - - C{lt}: less than - - - C{gt}: greater than - - - C{le}: less than or equal to - - - C{ge}: greater than or equal to - - - C{in}: checks if the value of an attribute is in a given list - - - C{notin}: checks if the value of an attribute is not in a given - list - - For instance, if you want to filter edges with a numeric C{weight} - property larger than 50, you have to write: - - >>> g.es.select(weight_gt=50) #doctest: +SKIP - - Similarly, to filter edges whose C{type} is in a list of predefined - types: - - >>> list_of_types = ["inhibitory", "excitatory"] - >>> g.es.select(type_in=list_of_types) #doctest: +SKIP - - If the operator is omitted, it defaults to C{eq}. For instance, the - following selector selects edges whose C{type} property is - C{intracluster}: - - >>> g.es.select(type="intracluster") #doctest: +SKIP - - In the case of an unknown operator, it is assumed that the - recognized operator is part of the attribute name and the actual - operator is C{eq}. - - Keyword arguments are treated specially if they start with an - underscore (C{_}). These are not real attributes but refer to specific - properties of the edges, e.g., their centrality. The rules are as - follows: - - 1. C{_source} or {_from} means the source vertex of an edge. For - undirected graphs, only the C{eq} operator is supported and it - is treated as {_incident} (since undirected graphs have no notion - of edge directionality). - - 2. C{_target} or {_to} means the target vertex of an edge. For - undirected graphs, only the C{eq} operator is supported and it - is treated as {_incident} (since undirected graphs have no notion - of edge directionality). - - 3. C{_within} ignores the operator and checks whether both endpoints - of the edge lie within a specified set. - - 4. C{_between} ignores the operator and checks whether I{one} - endpoint of the edge lies within a specified set and the I{other} - endpoint lies within another specified set. The two sets must be - given as a tuple. - - 5. C{_incident} ignores the operator and checks whether the edge is - incident on a specific vertex or a set of vertices. - - 6. Otherwise, the rest of the name is interpreted as a method of the - L{Graph} object. This method is called with the edge sequence as - its first argument (all others left at default values) and edges - are filtered according to the value returned by the method. - - For instance, if you want to exclude edges with a betweenness - centrality less than 2: - - >>> g = Graph.Famous("zachary") - >>> excl = g.es.select(_edge_betweenness_ge = 2) - - To select edges originating from vertices 2 and 4: - - >>> edges = g.es.select(_source_in = [2, 4]) - - To select edges lying entirely within the subgraph spanned by vertices - 2, 3, 4 and 7: - - >>> edges = g.es.select(_within = [2, 3, 4, 7]) - - To select edges with one endpoint in the vertex set containing vertices - 2, 3, 4 and 7 and the other endpoint in the vertex set containing - vertices 8 and 9: - - >>> edges = g.es.select(_between = ([2, 3, 4, 7], [8, 9])) - - For properties that take a long time to be computed (e.g., betweenness - centrality for large graphs), it is advised to calculate the values - in advance and store it in a graph attribute. The same applies when - you are selecting based on the same property more than once in the - same C{select()} call to avoid calculating it twice unnecessarily. - For instance, the following would calculate betweenness centralities - twice: - - >>> edges = g.es.select(_edge_betweenness_gt=10, # doctest:+SKIP - ... _edge_betweenness_lt=30) - - It is advised to use this instead: - - >>> g.es["bs"] = g.edge_betweenness() - >>> edges = g.es.select(bs_gt=10, bs_lt=30) - - @return: the new, filtered edge sequence - """ - es = _EdgeSeq.select(self, *args) - is_directed = self.graph.is_directed() - - def _ensure_set(value): - if isinstance(value, VertexSeq): - value = set(v.index for v in value) - elif not isinstance(value, (set, frozenset)): - value = set(value) - return value - - operators = { - "lt": operator.lt, - "gt": operator.gt, - "le": operator.le, - "ge": operator.ge, - "eq": operator.eq, - "ne": operator.ne, - "in": lambda a, b: a in b, - "notin": lambda a, b: a not in b, - } - - # TODO(ntamas): some keyword arguments should be prioritized over - # others; for instance, we have optimized code paths for _source and - # _target in directed and undirected graphs if es.is_all() is True; - # these should be executed first. This matters only if there are - # multiple keyword arguments and es.is_all() is True. - - for keyword, value in kwds.items(): - if "_" not in keyword or keyword.rindex("_") == 0: - keyword = keyword + "_eq" - pos = keyword.rindex("_") - attr, op = keyword[0:pos], keyword[pos + 1 :] - try: - func = operators[op] - except KeyError: - # No such operator, assume that it's part of the attribute name - attr, op, func = keyword, "eq", operators["eq"] - - if attr[0] == "_": - if attr in ("_source", "_from", "_target", "_to") and not is_directed: - if op not in ("eq", "in"): - raise RuntimeError("unsupported for undirected graphs") - - # translate to _incident to avoid confusion - attr = "_incident" - if func == operators["eq"]: - if hasattr(value, "__iter__") and not isinstance(value, str): - value = set(value) - else: - value = set([value]) - - if attr in ("_source", "_from"): - if es.is_all() and op == "eq": - # shortcut here: use .incident() as it is much faster - filtered_idxs = sorted(es.graph.incident(value, mode="out")) - func = None - # TODO(ntamas): there are more possibilities; we could - # optimize "ne", "in" and "notin" in similar ways - else: - values = [e.source for e in es] - if op == "in" or op == "notin": - value = _ensure_set(value) - - elif attr in ("_target", "_to"): - if es.is_all() and op == "eq": - # shortcut here: use .incident() as it is much faster - filtered_idxs = sorted(es.graph.incident(value, mode="in")) - func = None - # TODO(ntamas): there are more possibilities; we could - # optimize "ne", "in" and "notin" in similar ways - else: - values = [e.target for e in es] - if op == "in" or op == "notin": - value = _ensure_set(value) - - elif attr == "_incident": - func = None # ignoring function, filtering here - value = _ensure_set(value) - - # Fetch all the edges that are incident on at least one of - # the vertices specified - candidates = set() - for v in value: - candidates.update(es.graph.incident(v)) - - if not es.is_all(): - # Find those that are in the current edge sequence - filtered_idxs = [ - i for i, e in enumerate(es) if e.index in candidates - ] - else: - # We are done, the filtered indexes are in the candidates set - filtered_idxs = sorted(candidates) - - elif attr == "_within": - func = None # ignoring function, filtering here - value = _ensure_set(value) - - # Fetch all the edges that are incident on at least one of - # the vertices specified - candidates = set() - for v in value: - candidates.update(es.graph.incident(v)) - - if not es.is_all(): - # Find those where both endpoints are OK - filtered_idxs = [ - i - for i, e in enumerate(es) - if e.index in candidates - and e.source in value - and e.target in value - ] - else: - # Optimized version when the edge sequence contains all - # the edges exactly once in increasing order of edge IDs - filtered_idxs = [ - i - for i in candidates - if es[i].source in value and es[i].target in value - ] - - elif attr == "_between": - if len(value) != 2: - raise ValueError( - "_between selector requires two vertex ID lists" - ) - func = None # ignoring function, filtering here - set1 = _ensure_set(value[0]) - set2 = _ensure_set(value[1]) - - # Fetch all the edges that are incident on at least one of - # the vertices specified - candidates = set() - for v in set1: - candidates.update(es.graph.incident(v)) - for v in set2: - candidates.update(es.graph.incident(v)) - - if not es.is_all(): - # Find those where both endpoints are OK - filtered_idxs = [ - i - for i, e in enumerate(es) - if (e.source in set1 and e.target in set2) - or (e.target in set1 and e.source in set2) - ] - else: - # Optimized version when the edge sequence contains all - # the edges exactly once in increasing order of edge IDs - filtered_idxs = [ - i - for i in candidates - if (es[i].source in set1 and es[i].target in set2) - or (es[i].target in set1 and es[i].source in set2) - ] - - else: - # Method call, not an attribute - values = getattr(es.graph, attr[1:])(es) - else: - values = es[attr] - - # If we have a function to apply on the values, do that; otherwise - # we assume that filtered_idxs has already been calculated. - if func is not None: - filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] - - es = es.select(filtered_idxs) - - return es - - def __call__(self, *args, **kwds): - """Shorthand notation to select() - - This method simply passes all its arguments to L{EdgeSeq.select()}. - """ - return self.select(*args, **kwds) - - ############################################################## # Additional methods of VertexSeq and EdgeSeq that call Graph methods @@ -3172,69 +2125,6 @@ def result(*args, **kwds): ############################################################## -def autocurve(graph, attribute="curved", default=0): - """Calculates curvature values for each of the edges in the graph to make - sure that multiple edges are shown properly on a graph plot. - - This function checks the multiplicity of each edge in the graph and - assigns curvature values (numbers between -1 and 1, corresponding to - CCW (-1), straight (0) and CW (1) curved edges) to them. The assigned - values are either stored in an edge attribute or returned as a list, - depending on the value of the I{attribute} argument. - - @param graph: the graph on which the calculation will be run - @param attribute: the name of the edge attribute to save the curvature - values to. The default value is C{curved}, which is the name of the - edge attribute the default graph plotter checks to decide whether an - edge should be curved on the plot or not. If I{attribute} is C{None}, - the result will not be stored. - @param default: the default curvature for single edges. Zero means that - single edges will be straight. If you want single edges to be curved - as well, try passing 0.5 or -0.5 here. - @return: the list of curvature values if I{attribute} is C{None}, - otherwise C{None}. - """ - - # The following loop could be re-written in C if it turns out to be a - # bottleneck. Unfortunately we cannot use Graph.count_multiple() here - # because we have to ignore edge directions. - multiplicities = defaultdict(list) - for edge in graph.es: - u, v = edge.tuple - if u > v: - multiplicities[v, u].append(edge.index) - else: - multiplicities[u, v].append(edge.index) - - result = [default] * graph.ecount() - for eids in multiplicities.values(): - # Is it a single edge? - if len(eids) < 2: - continue - - if len(eids) % 2 == 1: - # Odd number of edges; the last will be straight - result[eids.pop()] = 0 - - # Arrange the remaining edges - curve = 2.0 / (len(eids) + 2) - dcurve, sign = curve, 1 - for idx, eid in enumerate(eids): - edge = graph.es[eid] - if edge.source > edge.target: - result[eid] = -sign * curve - else: - result[eid] = sign * curve - if idx % 2 == 1: - curve += dcurve - sign *= -1 - - if attribute is None: - return result - - graph.es[attribute] = result - - def get_include(): """Returns the folder that contains the C API headers of the Python interface of igraph.""" @@ -3330,4 +2220,6 @@ def write(graph, filename, *args, **kwds): _community_walktrap, _k_core, _community_leiden, + _graph_plot, + _operator_method_registry, ) diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index cdca21f0d..6c3732a38 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -17,7 +17,7 @@ from igraph.drawing.baseclasses import AbstractGraphDrawer, AbstractXMLRPCDrawer -__all__ = ("CytoscapeGraphDrawer",) +__all__ = ("CytoscapeGraphDrawer", "__plot__") class CytoscapeGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): @@ -356,3 +356,203 @@ def draw(self, graph, *args, **kwds): ) self.streamer.post(graph, self.connection, encoder=kwds.get("encoder")) + + +def __plot__(self, backend, context, *args, **kwds): + """Plots the graph to the given Cairo context or matplotlib Axes. + + The visual style of vertices and edges can be modified at three + places in the following order of precedence (lower indices override + higher indices): + + 1. Keyword arguments of this function (or of L{plot()} which is + passed intact to C{Graph.__plot__()}. + + 2. Vertex or edge attributes, specified later in the list of + keyword arguments. + + 3. igraph-wide plotting defaults (see + L{igraph.config.Configuration}) + + 4. Built-in defaults. + + E.g., if the C{vertex_size} keyword attribute is not present, + but there exists a vertex attribute named C{size}, the sizes of + the vertices will be specified by that attribute. + + Besides the usual self-explanatory plotting parameters (C{context}, + C{bbox}, C{palette}), it accepts the following keyword arguments: + + - C{autocurve}: whether to use curves instead of straight lines for + multiple edges on the graph plot. This argument may be C{True} + or C{False}; when omitted, C{True} is assumed for graphs with + less than 10.000 edges and C{False} otherwise. + + - C{drawer_factory}: a subclass of L{AbstractCairoGraphDrawer} + which will be used to draw the graph. You may also provide + a function here which takes two arguments: the Cairo context + to draw on and a bounding box (an instance of L{BoundingBox}). + If this keyword argument is missing, igraph will use the + default graph drawer which should be suitable for most purposes. + It is safe to omit this keyword argument unless you need to use + a specific graph drawer. + + - C{keep_aspect_ratio}: whether to keep the aspect ratio of the layout + that igraph calculates to place the nodes. C{True} means that the + layout will be scaled proportionally to fit into the bounding box + where the graph is to be drawn but the aspect ratio will be kept + the same (potentially leaving empty space next to, below or above + the graph). C{False} means that the layout will be scaled independently + along the X and Y axis in order to fill the entire bounding box. + The default is C{False}. + + - C{layout}: the layout to be used. If not an instance of + L{Layout}, it will be passed to L{layout} to calculate + the layout. Note that if you want a deterministic layout that + does not change with every plot, you must either use a + deterministic layout function (like L{layout_circle}) or + calculate the layout in advance and pass a L{Layout} object here. + + - C{margin}: the top, right, bottom, left margins as a 4-tuple. + If it has less than 4 elements or is a single float, the elements + will be re-used until the length is at least 4. + + - C{mark_groups}: whether to highlight some of the vertex groups by + colored polygons. This argument can be one of the following: + + - C{False}: no groups will be highlighted + + - C{True}: only valid if the object plotted is a + L{VertexClustering} or L{VertexCover}. The vertex groups in the + clutering or cover will be highlighted such that the i-th + group will be colored by the i-th color from the current + palette. If used when plotting a graph, it will throw an error. + + - A dict mapping tuples of vertex indices to color names. + The given vertex groups will be highlighted by the given + colors. + + - A list containing pairs or an iterable yielding pairs, where + the first element of each pair is a list of vertex indices and + the second element is a color. + + - A L{VertexClustering} or L{VertexCover} instance. The vertex + groups in the clustering or cover will be highlighted such that + the i-th group will be colored by the i-th color from the + current palette. + + In place of lists of vertex indices, you may also use L{VertexSeq} + instances. + + In place of color names, you may also use color indices into the + current palette. C{None} as a color name will mean that the + corresponding group is ignored. + + - C{vertex_size}: size of the vertices. The corresponding vertex + attribute is called C{size}. The default is 10. Vertex sizes + are measured in the unit of the Cairo context on which igraph + is drawing. + + - C{vertex_color}: color of the vertices. The corresponding vertex + attribute is C{color}, the default is red. Colors can be + specified either by common X11 color names (see the source + code of L{igraph.drawing.colors} for a list of known colors), by + 3-tuples of floats (ranging between 0 and 255 for the R, G and + B components), by CSS-style string specifications (C{#rrggbb}) + or by integer color indices of the specified palette. + + - C{vertex_frame_color}: color of the frame (i.e. stroke) of the + vertices. The corresponding vertex attribute is C{frame_color}, + the default is black. See C{vertex_color} for the possible ways + of specifying a color. + + - C{vertex_frame_width}: the width of the frame (i.e. stroke) of the + vertices. The corresponding vertex attribute is C{frame_width}. + The default is 1. Vertex frame widths are measured in the unit of the + Cairo context on which igraph is drawing. + + - C{vertex_shape}: shape of the vertices. Alternatively it can + be specified by the C{shape} vertex attribute. Possibilities + are: C{square}, {circle}, {triangle}, {triangle-down} or + C{hidden}. See the source code of L{igraph.drawing} for a + list of alternative shape names that are also accepted and + mapped to these. + + - C{vertex_label}: labels drawn next to the vertices. + The corresponding vertex attribute is C{label}. + + - C{vertex_label_dist}: distance of the midpoint of the vertex + label from the center of the corresponding vertex. + The corresponding vertex attribute is C{label_dist}. + + - C{vertex_label_color}: color of the label. Corresponding + vertex attribute: C{label_color}. See C{vertex_color} for + color specification syntax. + + - C{vertex_label_size}: font size of the label, specified + in the unit of the Cairo context on which we are drawing. + Corresponding vertex attribute: C{label_size}. + + - C{vertex_label_angle}: the direction of the line connecting + the midpoint of the vertex with the midpoint of the label. + This can be used to position the labels relative to the + vertices themselves in conjunction with C{vertex_label_dist}. + Corresponding vertex attribute: C{label_angle}. The + default is C{-math.pi/2}. + + - C{vertex_order}: drawing order of the vertices. This must be + a list or tuple containing vertex indices; vertices are then + drawn according to this order. + + - C{vertex_order_by}: an alternative way to specify the drawing + order of the vertices; this attribute is interpreted as the name + of a vertex attribute, and vertices are drawn such that those + with a smaller attribute value are drawn first. You may also + reverse the order by passing a tuple here; the first element of + the tuple should be the name of the attribute, the second element + specifies whether the order is reversed (C{True}, C{False}, + C{"asc"} and C{"desc"} are accepted values). + + - C{edge_color}: color of the edges. The corresponding edge + attribute is C{color}, the default is red. See C{vertex_color} + for color specification syntax. + + - C{edge_curved}: whether the edges should be curved. Positive + numbers correspond to edges curved in a counter-clockwise + direction, negative numbers correspond to edges curved in a + clockwise direction. Zero represents straight edges. C{True} + is interpreted as 0.5, C{False} is interpreted as 0. The + default is 0 which makes all the edges straight. + + - C{edge_width}: width of the edges in the default unit of the + Cairo context on which we are drawing. The corresponding + edge attribute is C{width}, the default is 1. + + - C{edge_arrow_size}: arrow size of the edges. The + corresponding edge attribute is C{arrow_size}, the default + is 1. + + - C{edge_arrow_width}: width of the arrowhead on the edge. The + corresponding edge attribute is C{arrow_width}, the default + is 1. + + - C{edge_order}: drawing order of the edges. This must be + a list or tuple containing edge indices; edges are then + drawn according to this order. + + - C{edge_order_by}: an alternative way to specify the drawing + order of the edges; this attribute is interpreted as the name + of an edge attribute, and edges are drawn such that those + with a smaller attribute value are drawn first. You may also + reverse the order by passing a tuple here; the first element of + the tuple should be the name of the attribute, the second element + specifies whether the order is reversed (C{True}, C{False}, + C{"asc"} and C{"desc"} are accepted values). + """ + from igraph.drawing import DrawerDirectory + + drawer = kwds.pop( + "drawer_factory", + DrawerDirectory.resolve(self, backend)(context), + ) + drawer.draw(self, *args, **kwds) diff --git a/src/igraph/drawing/utils.py b/src/igraph/drawing/utils.py index 216d13298..c7ef8fa2d 100644 --- a/src/igraph/drawing/utils.py +++ b/src/igraph/drawing/utils.py @@ -2,6 +2,7 @@ Utility classes for drawing routines. """ +from collections import defaultdict from math import atan2, cos, hypot, sin from typing import NamedTuple @@ -16,6 +17,7 @@ "evaluate_cubic_bezier", "intersect_bezier_curve_and_circle", "str_to_orientation", + "autocurve", ) ##################################################################### @@ -665,3 +667,66 @@ def str_to_orientation(value, reversed_horizontal=False, reversed_vertical=False if result not in ("lr", "rl", "tb", "bt"): raise ValueError("unknown orientation: %s" % result) return result + + +def autocurve(graph, attribute="curved", default=0): + """Calculates curvature values for each of the edges in the graph to make + sure that multiple edges are shown properly on a graph plot. + + This function checks the multiplicity of each edge in the graph and + assigns curvature values (numbers between -1 and 1, corresponding to + CCW (-1), straight (0) and CW (1) curved edges) to them. The assigned + values are either stored in an edge attribute or returned as a list, + depending on the value of the I{attribute} argument. + + @param graph: the graph on which the calculation will be run + @param attribute: the name of the edge attribute to save the curvature + values to. The default value is C{curved}, which is the name of the + edge attribute the default graph plotter checks to decide whether an + edge should be curved on the plot or not. If I{attribute} is C{None}, + the result will not be stored. + @param default: the default curvature for single edges. Zero means that + single edges will be straight. If you want single edges to be curved + as well, try passing 0.5 or -0.5 here. + @return: the list of curvature values if I{attribute} is C{None}, + otherwise C{None}. + """ + + # The following loop could be re-written in C if it turns out to be a + # bottleneck. Unfortunately we cannot use Graph.count_multiple() here + # because we have to ignore edge directions. + multiplicities = defaultdict(list) + for edge in graph.es: + u, v = edge.tuple + if u > v: + multiplicities[v, u].append(edge.index) + else: + multiplicities[u, v].append(edge.index) + + result = [default] * graph.ecount() + for eids in multiplicities.values(): + # Is it a single edge? + if len(eids) < 2: + continue + + if len(eids) % 2 == 1: + # Odd number of edges; the last will be straight + result[eids.pop()] = 0 + + # Arrange the remaining edges + curve = 2.0 / (len(eids) + 2) + dcurve, sign = curve, 1 + for idx, eid in enumerate(eids): + edge = graph.es[eid] + if edge.source > edge.target: + result[eid] = -sign * curve + else: + result[eid] = sign * curve + if idx % 2 == 1: + curve += dcurve + sign *= -1 + + if attribute is None: + return result + + graph.es[attribute] = result diff --git a/src/igraph/operators/__init__.py b/src/igraph/operators/__init__.py new file mode 100644 index 000000000..cb3407a4c --- /dev/null +++ b/src/igraph/operators/__init__.py @@ -0,0 +1,35 @@ +# vim:ts=4:sw=4:sts=4:et +# -*- coding: utf-8 -*- +"""Implementation of union, disjoint union and intersection operators.""" + +__all__ = ( + "disjoint_union", + "union", + "intersection", + "operator_method_registry", + ) + +from igraph.operators.functions import ( + disjoint_union, + union, + intersection, +) +from igraph.operators.methods import ( + __iadd__, + __add__, + __and__, + __isub__, + __sub__, + __mul__, + __or__, +) + +operator_method_registry = { + '__iadd__': __iadd__, + '__add__': __add__, + '__and__': __and__, + '__isub__': __isub__, + '__sub__': __sub__, + '__mul__': __mul__, + '__or__': __or__, +} diff --git a/src/igraph/operators.py b/src/igraph/operators/functions.py similarity index 100% rename from src/igraph/operators.py rename to src/igraph/operators/functions.py diff --git a/src/igraph/operators/methods.py b/src/igraph/operators/methods.py new file mode 100644 index 000000000..86ccd2821 --- /dev/null +++ b/src/igraph/operators/methods.py @@ -0,0 +1,211 @@ +from igraph._igraph import ( + Vertex, + Edge, +) +from igraph.seq import VertexSeq, EdgeSeq + + +__all__ = ( + "__iadd__", + "__add__", + "__and__", + "__isub__", + "__sub__", + "__mul__", + "__or__", +) + + +def __iadd__(self, other): + """In-place addition (disjoint union). + + @see: L{__add__} + """ + if isinstance(other, (int, str)): + self.add_vertices(other) + return self + elif isinstance(other, tuple) and len(other) == 2: + self.add_edges([other]) + return self + elif isinstance(other, list): + if not other: + return self + if isinstance(other[0], tuple): + self.add_edges(other) + return self + if isinstance(other[0], str): + self.add_vertices(other) + return self + return NotImplemented + + +def __add__(self, other): + """Copies the graph and extends the copy depending on the type of + the other object given. + + @param other: if it is an integer, the copy is extended by the given + number of vertices. If it is a string, the copy is extended by a + single vertex whose C{name} attribute will be equal to the given + string. If it is a tuple with two elements, the copy + is extended by a single edge. If it is a list of tuples, the copy + is extended by multiple edges. If it is a L{Graph}, a disjoint + union is performed. + """ + # Deferred import to avoid cycles + from igraph import Graph + + if isinstance(other, (int, str)): + g = self.copy() + g.add_vertices(other) + elif isinstance(other, tuple) and len(other) == 2: + g = self.copy() + g.add_edges([other]) + elif isinstance(other, list): + if len(other) > 0: + if isinstance(other[0], tuple): + g = self.copy() + g.add_edges(other) + elif isinstance(other[0], str): + g = self.copy() + g.add_vertices(other) + elif isinstance(other[0], Graph): + return self.disjoint_union(other) + else: + return NotImplemented + else: + return self.copy() + + elif isinstance(other, Graph): + return self.disjoint_union(other) + else: + return NotImplemented + + return g + + +def __and__(self, other): + """Graph intersection operator. + + @param other: the other graph to take the intersection with. + @return: the intersected graph. + """ + # Deferred import to avoid cycles + from igraph import Graph + + if isinstance(other, Graph): + return self.intersection(other) + else: + return NotImplemented + + +def __isub__(self, other): + """In-place subtraction (difference). + + @see: L{__sub__}""" + if isinstance(other, int): + self.delete_vertices([other]) + elif isinstance(other, tuple) and len(other) == 2: + self.delete_edges([other]) + elif isinstance(other, list): + if len(other) > 0: + if isinstance(other[0], tuple): + self.delete_edges(other) + elif isinstance(other[0], (int, str)): + self.delete_vertices(other) + else: + return NotImplemented + elif isinstance(other, Vertex): + self.delete_vertices(other) + elif isinstance(other, VertexSeq): + self.delete_vertices(other) + elif isinstance(other, Edge): + self.delete_edges(other) + elif isinstance(other, EdgeSeq): + self.delete_edges(other) + else: + return NotImplemented + return self + + +def __sub__(self, other): + """Removes the given object(s) from the graph + + @param other: if it is an integer, removes the vertex with the given + ID from the graph (note that the remaining vertices will get + re-indexed!). If it is a tuple, removes the given edge. If it is + a graph, takes the difference of the two graphs. Accepts + lists of integers or lists of tuples as well, but they can't be + mixed! Also accepts L{Edge} and L{EdgeSeq} objects. + """ + # Deferred import to avoid cycles + from igraph import Graph + + if isinstance(other, Graph): + return self.difference(other) + + result = self.copy() + if isinstance(other, (int, str)): + result.delete_vertices([other]) + elif isinstance(other, tuple) and len(other) == 2: + result.delete_edges([other]) + elif isinstance(other, list): + if len(other) > 0: + if isinstance(other[0], tuple): + result.delete_edges(other) + elif isinstance(other[0], (int, str)): + result.delete_vertices(other) + else: + return NotImplemented + else: + return result + elif isinstance(other, Vertex): + result.delete_vertices(other) + elif isinstance(other, VertexSeq): + result.delete_vertices(other) + elif isinstance(other, Edge): + result.delete_edges(other) + elif isinstance(other, EdgeSeq): + result.delete_edges(other) + else: + return NotImplemented + + return result + + +def __mul__(self, other): + """Copies exact replicas of the original graph an arbitrary number of + times. + + @param other: if it is an integer, multiplies the graph by creating the + given number of identical copies and taking the disjoint union of + them. + """ + # Deferred import to avoid cycles + from igraph import Graph + + if isinstance(other, int): + if other == 0: + return Graph() + elif other == 1: + return self + elif other > 1: + return self.disjoint_union([self] * (other - 1)) + else: + return NotImplemented + + return NotImplemented + + +def __or__(self, other): + """Graph union operator. + + @param other: the other graph to take the union with. + @return: the union graph. + """ + # Deferred import to avoid cycles + from igraph import Graph + + if isinstance(other, Graph): + return self.union(other) + else: + return NotImplemented diff --git a/src/igraph/seq/__init__.py b/src/igraph/seq/__init__.py new file mode 100644 index 000000000..de065bb79 --- /dev/null +++ b/src/igraph/seq/__init__.py @@ -0,0 +1,700 @@ +import operator + +from igraph._igraph import ( + EdgeSeq as _EdgeSeq, + VertexSeq as _VertexSeq, +) + + +class VertexSeq(_VertexSeq): + """Class representing a sequence of vertices in the graph. + + This class is most easily accessed by the C{vs} field of the + L{Graph} object, which returns an ordered sequence of all vertices in + the graph. The vertex sequence can be refined by invoking the + L{VertexSeq.select()} method. L{VertexSeq.select()} can also be + accessed by simply calling the L{VertexSeq} object. + + An alternative way to create a vertex sequence referring to a given + graph is to use the constructor directly: + + >>> g = Graph.Full(3) + >>> vs = VertexSeq(g) + >>> restricted_vs = VertexSeq(g, [0, 1]) + + The individual vertices can be accessed by indexing the vertex sequence + object. It can be used as an iterable as well, or even in a list + comprehension: + + >>> g=Graph.Full(3) + >>> for v in g.vs: + ... v["value"] = v.index ** 2 + ... + >>> [v["value"] ** 0.5 for v in g.vs] + [0.0, 1.0, 2.0] + + The vertex set can also be used as a dictionary where the keys are the + attribute names. The values corresponding to the keys are the values + of the given attribute for every vertex selected by the sequence. + + >>> g=Graph.Full(3) + >>> for idx, v in enumerate(g.vs): + ... v["weight"] = idx*(idx+1) + ... + >>> g.vs["weight"] + [0, 2, 6] + >>> g.vs.select(1,2)["weight"] = [10, 20] + >>> g.vs["weight"] + [0, 10, 20] + + If you specify a sequence that is shorter than the number of vertices in + the VertexSeq, the sequence is reused: + + >>> g = Graph.Tree(7, 2) + >>> g.vs["color"] = ["red", "green"] + >>> g.vs["color"] + ['red', 'green', 'red', 'green', 'red', 'green', 'red'] + + You can even pass a single string or integer, it will be considered as a + sequence of length 1: + + >>> g.vs["color"] = "red" + >>> g.vs["color"] + ['red', 'red', 'red', 'red', 'red', 'red', 'red'] + + Some methods of the vertex sequences are simply proxy methods to the + corresponding methods in the L{Graph} object. One such example is + C{VertexSeq.degree()}: + + >>> g=Graph.Tree(7, 2) + >>> g.vs.degree() + [2, 3, 3, 1, 1, 1, 1] + >>> g.vs.degree() == g.degree() + True + """ + + def attributes(self): + """Returns the list of all the vertex attributes in the graph + associated to this vertex sequence.""" + return self.graph.vertex_attributes() + + def find(self, *args, **kwds): + """Returns the first vertex of the vertex sequence that matches some + criteria. + + The selection criteria are equal to the ones allowed by L{VertexSeq.select}. + See L{VertexSeq.select} for more details. + + For instance, to find the first vertex with name C{foo} in graph C{g}: + + >>> g.vs.find(name="foo") #doctest:+SKIP + + To find an arbitrary isolated vertex: + + >>> g.vs.find(_degree=0) #doctest:+SKIP + """ + # Shortcut: if "name" is in kwds, there are no positional arguments, + # and the specified name is a string, we try that first because that + # attribute is indexed. Note that we cannot do this if name is an + # integer, because it would then translate to g.vs.select(name), which + # searches by _index_ if the argument is an integer + if not args: + if "name" in kwds: + name = kwds.pop("name") + elif "name_eq" in kwds: + name = kwds.pop("name_eq") + else: + name = None + + if name is not None: + if isinstance(name, str): + args = [name] + else: + # put back what we popped + kwds["name"] = name + + if args: + # Selecting first based on positional arguments, then checking + # the criteria specified by the (remaining) keyword arguments + vertex = _VertexSeq.find(self, *args) + if not kwds: + return vertex + vs = self.graph.vs.select(vertex.index) + else: + vs = self + + # Selecting based on keyword arguments + vs = vs.select(**kwds) + if vs: + return vs[0] + raise ValueError("no such vertex") + + def select(self, *args, **kwds): + """Selects a subset of the vertex sequence based on some criteria + + The selection criteria can be specified by the positional and the keyword + arguments. Positional arguments are always processed before keyword + arguments. + + - If the first positional argument is C{None}, an empty sequence is + returned. + + - If the first positional argument is a callable object, the object + will be called for every vertex in the sequence. If it returns + C{True}, the vertex will be included, otherwise it will + be excluded. + + - If the first positional argument is an iterable, it must return + integers and they will be considered as indices of the current + vertex set (NOT the whole vertex set of the graph -- the + difference matters when one filters a vertex set that has + already been filtered by a previous invocation of + L{VertexSeq.select()}. In this case, the indices do not refer + directly to the vertices of the graph but to the elements of + the filtered vertex sequence. + + - If the first positional argument is an integer, all remaining + arguments are expected to be integers. They are considered as + indices of the current vertex set again. + + Keyword arguments can be used to filter the vertices based on their + attributes. The name of the keyword specifies the name of the attribute + and the filtering operator, they should be concatenated by an + underscore (C{_}) character. Attribute names can also contain + underscores, but operator names don't, so the operator is always the + largest trailing substring of the keyword name that does not contain + an underscore. Possible operators are: + + - C{eq}: equal to + + - C{ne}: not equal to + + - C{lt}: less than + + - C{gt}: greater than + + - C{le}: less than or equal to + + - C{ge}: greater than or equal to + + - C{in}: checks if the value of an attribute is in a given list + + - C{notin}: checks if the value of an attribute is not in a given + list + + For instance, if you want to filter vertices with a numeric C{age} + property larger than 200, you have to write: + + >>> g.vs.select(age_gt=200) #doctest: +SKIP + + Similarly, to filter vertices whose C{type} is in a list of predefined + types: + + >>> list_of_types = ["HR", "Finance", "Management"] + >>> g.vs.select(type_in=list_of_types) #doctest: +SKIP + + If the operator is omitted, it defaults to C{eq}. For instance, the + following selector selects vertices whose C{cluster} property equals + to 2: + + >>> g.vs.select(cluster=2) #doctest: +SKIP + + In the case of an unknown operator, it is assumed that the + recognized operator is part of the attribute name and the actual + operator is C{eq}. + + Attribute names inferred from keyword arguments are treated specially + if they start with an underscore (C{_}). These are not real attributes + but refer to specific properties of the vertices, e.g., its degree. + The rule is as follows: if an attribute name starts with an underscore, + the rest of the name is interpreted as a method of the L{Graph} object. + This method is called with the vertex sequence as its first argument + (all others left at default values) and vertices are filtered + according to the value returned by the method. For instance, if you + want to exclude isolated vertices: + + >>> g = Graph.Famous("zachary") + >>> non_isolated = g.vs.select(_degree_gt=0) + + For properties that take a long time to be computed (e.g., betweenness + centrality for large graphs), it is advised to calculate the values + in advance and store it in a graph attribute. The same applies when + you are selecting based on the same property more than once in the + same C{select()} call to avoid calculating it twice unnecessarily. + For instance, the following would calculate betweenness centralities + twice: + + >>> edges = g.vs.select(_betweenness_gt=10, _betweenness_lt=30) + + It is advised to use this instead: + + >>> g.vs["bs"] = g.betweenness() + >>> edges = g.vs.select(bs_gt=10, bs_lt=30) + + @return: the new, filtered vertex sequence""" + vs = _VertexSeq.select(self, *args) + + operators = { + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "eq": operator.eq, + "ne": operator.ne, + "in": lambda a, b: a in b, + "notin": lambda a, b: a not in b, + } + for keyword, value in kwds.items(): + if "_" not in keyword or keyword.rindex("_") == 0: + keyword = keyword + "_eq" + attr, _, op = keyword.rpartition("_") + try: + func = operators[op] + except KeyError: + # No such operator, assume that it's part of the attribute name + attr, op, func = keyword, "eq", operators["eq"] + + if attr[0] == "_": + # Method call, not an attribute + values = getattr(vs.graph, attr[1:])(vs) + else: + values = vs[attr] + filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] + vs = vs.select(filtered_idxs) + + return vs + + def __call__(self, *args, **kwds): + """Shorthand notation to select() + + This method simply passes all its arguments to L{VertexSeq.select()}. + """ + return self.select(*args, **kwds) + + +class EdgeSeq(_EdgeSeq): + """Class representing a sequence of edges in the graph. + + This class is most easily accessed by the C{es} field of the + L{Graph} object, which returns an ordered sequence of all edges in + the graph. The edge sequence can be refined by invoking the + L{EdgeSeq.select()} method. L{EdgeSeq.select()} can also be + accessed by simply calling the L{EdgeSeq} object. + + An alternative way to create an edge sequence referring to a given + graph is to use the constructor directly: + + >>> g = Graph.Full(3) + >>> es = EdgeSeq(g) + >>> restricted_es = EdgeSeq(g, [0, 1]) + + The individual edges can be accessed by indexing the edge sequence + object. It can be used as an iterable as well, or even in a list + comprehension: + + >>> g=Graph.Full(3) + >>> for e in g.es: + ... print(e.tuple) + ... + (0, 1) + (0, 2) + (1, 2) + >>> [max(e.tuple) for e in g.es] + [1, 2, 2] + + The edge sequence can also be used as a dictionary where the keys are the + attribute names. The values corresponding to the keys are the values + of the given attribute of every edge in the graph: + + >>> g=Graph.Full(3) + >>> for idx, e in enumerate(g.es): + ... e["weight"] = idx*(idx+1) + ... + >>> g.es["weight"] + [0, 2, 6] + >>> g.es["weight"] = range(3) + >>> g.es["weight"] + [0, 1, 2] + + If you specify a sequence that is shorter than the number of edges in + the EdgeSeq, the sequence is reused: + + >>> g = Graph.Tree(7, 2) + >>> g.es["color"] = ["red", "green"] + >>> g.es["color"] + ['red', 'green', 'red', 'green', 'red', 'green'] + + You can even pass a single string or integer, it will be considered as a + sequence of length 1: + + >>> g.es["color"] = "red" + >>> g.es["color"] + ['red', 'red', 'red', 'red', 'red', 'red'] + + Some methods of the edge sequences are simply proxy methods to the + corresponding methods in the L{Graph} object. One such example is + C{EdgeSeq.is_multiple()}: + + >>> g=Graph(3, [(0,1), (1,0), (1,2)]) + >>> g.es.is_multiple() + [False, True, False] + >>> g.es.is_multiple() == g.is_multiple() + True + """ + + def attributes(self): + """Returns the list of all the edge attributes in the graph + associated to this edge sequence.""" + return self.graph.edge_attributes() + + def find(self, *args, **kwds): + """Returns the first edge of the edge sequence that matches some + criteria. + + The selection criteria are equal to the ones allowed by L{VertexSeq.select}. + See L{VertexSeq.select} for more details. + + For instance, to find the first edge with weight larger than 5 in graph C{g}: + + >>> g.es.find(weight_gt=5) #doctest:+SKIP + """ + if args: + # Selecting first based on positional arguments, then checking + # the criteria specified by the keyword arguments + edge = _EdgeSeq.find(self, *args) + if not kwds: + return edge + es = self.graph.es.select(edge.index) + else: + es = self + + # Selecting based on positional arguments + es = es.select(**kwds) + if es: + return es[0] + raise ValueError("no such edge") + + def select(self, *args, **kwds): + """Selects a subset of the edge sequence based on some criteria + + The selection criteria can be specified by the positional and the + keyword arguments. Positional arguments are always processed before + keyword arguments. + + - If the first positional argument is C{None}, an empty sequence is + returned. + + - If the first positional argument is a callable object, the object + will be called for every edge in the sequence. If it returns + C{True}, the edge will be included, otherwise it will + be excluded. + + - If the first positional argument is an iterable, it must return + integers and they will be considered as indices of the current + edge set (NOT the whole edge set of the graph -- the + difference matters when one filters an edge set that has + already been filtered by a previous invocation of + L{EdgeSeq.select()}. In this case, the indices do not refer + directly to the edges of the graph but to the elements of + the filtered edge sequence. + + - If the first positional argument is an integer, all remaining + arguments are expected to be integers. They are considered as + indices of the current edge set again. + + Keyword arguments can be used to filter the edges based on their + attributes and properties. The name of the keyword specifies the name + of the attribute and the filtering operator, they should be + concatenated by an underscore (C{_}) character. Attribute names can + also contain underscores, but operator names don't, so the operator is + always the largest trailing substring of the keyword name that does not + contain an underscore. Possible operators are: + + - C{eq}: equal to + + - C{ne}: not equal to + + - C{lt}: less than + + - C{gt}: greater than + + - C{le}: less than or equal to + + - C{ge}: greater than or equal to + + - C{in}: checks if the value of an attribute is in a given list + + - C{notin}: checks if the value of an attribute is not in a given + list + + For instance, if you want to filter edges with a numeric C{weight} + property larger than 50, you have to write: + + >>> g.es.select(weight_gt=50) #doctest: +SKIP + + Similarly, to filter edges whose C{type} is in a list of predefined + types: + + >>> list_of_types = ["inhibitory", "excitatory"] + >>> g.es.select(type_in=list_of_types) #doctest: +SKIP + + If the operator is omitted, it defaults to C{eq}. For instance, the + following selector selects edges whose C{type} property is + C{intracluster}: + + >>> g.es.select(type="intracluster") #doctest: +SKIP + + In the case of an unknown operator, it is assumed that the + recognized operator is part of the attribute name and the actual + operator is C{eq}. + + Keyword arguments are treated specially if they start with an + underscore (C{_}). These are not real attributes but refer to specific + properties of the edges, e.g., their centrality. The rules are as + follows: + + 1. C{_source} or {_from} means the source vertex of an edge. For + undirected graphs, only the C{eq} operator is supported and it + is treated as {_incident} (since undirected graphs have no notion + of edge directionality). + + 2. C{_target} or {_to} means the target vertex of an edge. For + undirected graphs, only the C{eq} operator is supported and it + is treated as {_incident} (since undirected graphs have no notion + of edge directionality). + + 3. C{_within} ignores the operator and checks whether both endpoints + of the edge lie within a specified set. + + 4. C{_between} ignores the operator and checks whether I{one} + endpoint of the edge lies within a specified set and the I{other} + endpoint lies within another specified set. The two sets must be + given as a tuple. + + 5. C{_incident} ignores the operator and checks whether the edge is + incident on a specific vertex or a set of vertices. + + 6. Otherwise, the rest of the name is interpreted as a method of the + L{Graph} object. This method is called with the edge sequence as + its first argument (all others left at default values) and edges + are filtered according to the value returned by the method. + + For instance, if you want to exclude edges with a betweenness + centrality less than 2: + + >>> g = Graph.Famous("zachary") + >>> excl = g.es.select(_edge_betweenness_ge = 2) + + To select edges originating from vertices 2 and 4: + + >>> edges = g.es.select(_source_in = [2, 4]) + + To select edges lying entirely within the subgraph spanned by vertices + 2, 3, 4 and 7: + + >>> edges = g.es.select(_within = [2, 3, 4, 7]) + + To select edges with one endpoint in the vertex set containing vertices + 2, 3, 4 and 7 and the other endpoint in the vertex set containing + vertices 8 and 9: + + >>> edges = g.es.select(_between = ([2, 3, 4, 7], [8, 9])) + + For properties that take a long time to be computed (e.g., betweenness + centrality for large graphs), it is advised to calculate the values + in advance and store it in a graph attribute. The same applies when + you are selecting based on the same property more than once in the + same C{select()} call to avoid calculating it twice unnecessarily. + For instance, the following would calculate betweenness centralities + twice: + + >>> edges = g.es.select(_edge_betweenness_gt=10, # doctest:+SKIP + ... _edge_betweenness_lt=30) + + It is advised to use this instead: + + >>> g.es["bs"] = g.edge_betweenness() + >>> edges = g.es.select(bs_gt=10, bs_lt=30) + + @return: the new, filtered edge sequence + """ + es = _EdgeSeq.select(self, *args) + is_directed = self.graph.is_directed() + + def _ensure_set(value): + if isinstance(value, VertexSeq): + value = set(v.index for v in value) + elif not isinstance(value, (set, frozenset)): + value = set(value) + return value + + operators = { + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "eq": operator.eq, + "ne": operator.ne, + "in": lambda a, b: a in b, + "notin": lambda a, b: a not in b, + } + + # TODO(ntamas): some keyword arguments should be prioritized over + # others; for instance, we have optimized code paths for _source and + # _target in directed and undirected graphs if es.is_all() is True; + # these should be executed first. This matters only if there are + # multiple keyword arguments and es.is_all() is True. + + for keyword, value in kwds.items(): + if "_" not in keyword or keyword.rindex("_") == 0: + keyword = keyword + "_eq" + pos = keyword.rindex("_") + attr, op = keyword[0:pos], keyword[pos + 1 :] + try: + func = operators[op] + except KeyError: + # No such operator, assume that it's part of the attribute name + attr, op, func = keyword, "eq", operators["eq"] + + if attr[0] == "_": + if attr in ("_source", "_from", "_target", "_to") and not is_directed: + if op not in ("eq", "in"): + raise RuntimeError("unsupported for undirected graphs") + + # translate to _incident to avoid confusion + attr = "_incident" + if func == operators["eq"]: + if hasattr(value, "__iter__") and not isinstance(value, str): + value = set(value) + else: + value = set([value]) + + if attr in ("_source", "_from"): + if es.is_all() and op == "eq": + # shortcut here: use .incident() as it is much faster + filtered_idxs = sorted(es.graph.incident(value, mode="out")) + func = None + # TODO(ntamas): there are more possibilities; we could + # optimize "ne", "in" and "notin" in similar ways + else: + values = [e.source for e in es] + if op == "in" or op == "notin": + value = _ensure_set(value) + + elif attr in ("_target", "_to"): + if es.is_all() and op == "eq": + # shortcut here: use .incident() as it is much faster + filtered_idxs = sorted(es.graph.incident(value, mode="in")) + func = None + # TODO(ntamas): there are more possibilities; we could + # optimize "ne", "in" and "notin" in similar ways + else: + values = [e.target for e in es] + if op == "in" or op == "notin": + value = _ensure_set(value) + + elif attr == "_incident": + func = None # ignoring function, filtering here + value = _ensure_set(value) + + # Fetch all the edges that are incident on at least one of + # the vertices specified + candidates = set() + for v in value: + candidates.update(es.graph.incident(v)) + + if not es.is_all(): + # Find those that are in the current edge sequence + filtered_idxs = [ + i for i, e in enumerate(es) if e.index in candidates + ] + else: + # We are done, the filtered indexes are in the candidates set + filtered_idxs = sorted(candidates) + + elif attr == "_within": + func = None # ignoring function, filtering here + value = _ensure_set(value) + + # Fetch all the edges that are incident on at least one of + # the vertices specified + candidates = set() + for v in value: + candidates.update(es.graph.incident(v)) + + if not es.is_all(): + # Find those where both endpoints are OK + filtered_idxs = [ + i + for i, e in enumerate(es) + if e.index in candidates + and e.source in value + and e.target in value + ] + else: + # Optimized version when the edge sequence contains all + # the edges exactly once in increasing order of edge IDs + filtered_idxs = [ + i + for i in candidates + if es[i].source in value and es[i].target in value + ] + + elif attr == "_between": + if len(value) != 2: + raise ValueError( + "_between selector requires two vertex ID lists" + ) + func = None # ignoring function, filtering here + set1 = _ensure_set(value[0]) + set2 = _ensure_set(value[1]) + + # Fetch all the edges that are incident on at least one of + # the vertices specified + candidates = set() + for v in set1: + candidates.update(es.graph.incident(v)) + for v in set2: + candidates.update(es.graph.incident(v)) + + if not es.is_all(): + # Find those where both endpoints are OK + filtered_idxs = [ + i + for i, e in enumerate(es) + if (e.source in set1 and e.target in set2) + or (e.target in set1 and e.source in set2) + ] + else: + # Optimized version when the edge sequence contains all + # the edges exactly once in increasing order of edge IDs + filtered_idxs = [ + i + for i in candidates + if (es[i].source in set1 and es[i].target in set2) + or (es[i].target in set1 and es[i].source in set2) + ] + + else: + # Method call, not an attribute + values = getattr(es.graph, attr[1:])(es) + else: + values = es[attr] + + # If we have a function to apply on the values, do that; otherwise + # we assume that filtered_idxs has already been calculated. + if func is not None: + filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] + + es = es.select(filtered_idxs) + + return es + + def __call__(self, *args, **kwds): + """Shorthand notation to select() + + This method simply passes all its arguments to L{EdgeSeq.select()}. + """ + return self.select(*args, **kwds) + + diff --git a/src/igraph/seq/edgeseq.py b/src/igraph/seq/edgeseq.py new file mode 100644 index 000000000..8ab03e2c2 --- /dev/null +++ b/src/igraph/seq/edgeseq.py @@ -0,0 +1,431 @@ +import operator + +from igraph._igraph import EdgeSeq as _EdgeSeq + + +class EdgeSeq(_EdgeSeq): + """Class representing a sequence of edges in the graph. + + This class is most easily accessed by the C{es} field of the + L{Graph} object, which returns an ordered sequence of all edges in + the graph. The edge sequence can be refined by invoking the + L{EdgeSeq.select()} method. L{EdgeSeq.select()} can also be + accessed by simply calling the L{EdgeSeq} object. + + An alternative way to create an edge sequence referring to a given + graph is to use the constructor directly: + + >>> g = Graph.Full(3) + >>> es = EdgeSeq(g) + >>> restricted_es = EdgeSeq(g, [0, 1]) + + The individual edges can be accessed by indexing the edge sequence + object. It can be used as an iterable as well, or even in a list + comprehension: + + >>> g=Graph.Full(3) + >>> for e in g.es: + ... print(e.tuple) + ... + (0, 1) + (0, 2) + (1, 2) + >>> [max(e.tuple) for e in g.es] + [1, 2, 2] + + The edge sequence can also be used as a dictionary where the keys are the + attribute names. The values corresponding to the keys are the values + of the given attribute of every edge in the graph: + + >>> g=Graph.Full(3) + >>> for idx, e in enumerate(g.es): + ... e["weight"] = idx*(idx+1) + ... + >>> g.es["weight"] + [0, 2, 6] + >>> g.es["weight"] = range(3) + >>> g.es["weight"] + [0, 1, 2] + + If you specify a sequence that is shorter than the number of edges in + the EdgeSeq, the sequence is reused: + + >>> g = Graph.Tree(7, 2) + >>> g.es["color"] = ["red", "green"] + >>> g.es["color"] + ['red', 'green', 'red', 'green', 'red', 'green'] + + You can even pass a single string or integer, it will be considered as a + sequence of length 1: + + >>> g.es["color"] = "red" + >>> g.es["color"] + ['red', 'red', 'red', 'red', 'red', 'red'] + + Some methods of the edge sequences are simply proxy methods to the + corresponding methods in the L{Graph} object. One such example is + C{EdgeSeq.is_multiple()}: + + >>> g=Graph(3, [(0,1), (1,0), (1,2)]) + >>> g.es.is_multiple() + [False, True, False] + >>> g.es.is_multiple() == g.is_multiple() + True + """ + + def attributes(self): + """Returns the list of all the edge attributes in the graph + associated to this edge sequence.""" + return self.graph.edge_attributes() + + def find(self, *args, **kwds): + """Returns the first edge of the edge sequence that matches some + criteria. + + The selection criteria are equal to the ones allowed by L{VertexSeq.select}. + See L{VertexSeq.select} for more details. + + For instance, to find the first edge with weight larger than 5 in graph C{g}: + + >>> g.es.find(weight_gt=5) #doctest:+SKIP + """ + if args: + # Selecting first based on positional arguments, then checking + # the criteria specified by the keyword arguments + edge = _EdgeSeq.find(self, *args) + if not kwds: + return edge + es = self.graph.es.select(edge.index) + else: + es = self + + # Selecting based on positional arguments + es = es.select(**kwds) + if es: + return es[0] + raise ValueError("no such edge") + + def select(self, *args, **kwds): + """Selects a subset of the edge sequence based on some criteria + + The selection criteria can be specified by the positional and the + keyword arguments. Positional arguments are always processed before + keyword arguments. + + - If the first positional argument is C{None}, an empty sequence is + returned. + + - If the first positional argument is a callable object, the object + will be called for every edge in the sequence. If it returns + C{True}, the edge will be included, otherwise it will + be excluded. + + - If the first positional argument is an iterable, it must return + integers and they will be considered as indices of the current + edge set (NOT the whole edge set of the graph -- the + difference matters when one filters an edge set that has + already been filtered by a previous invocation of + L{EdgeSeq.select()}. In this case, the indices do not refer + directly to the edges of the graph but to the elements of + the filtered edge sequence. + + - If the first positional argument is an integer, all remaining + arguments are expected to be integers. They are considered as + indices of the current edge set again. + + Keyword arguments can be used to filter the edges based on their + attributes and properties. The name of the keyword specifies the name + of the attribute and the filtering operator, they should be + concatenated by an underscore (C{_}) character. Attribute names can + also contain underscores, but operator names don't, so the operator is + always the largest trailing substring of the keyword name that does not + contain an underscore. Possible operators are: + + - C{eq}: equal to + + - C{ne}: not equal to + + - C{lt}: less than + + - C{gt}: greater than + + - C{le}: less than or equal to + + - C{ge}: greater than or equal to + + - C{in}: checks if the value of an attribute is in a given list + + - C{notin}: checks if the value of an attribute is not in a given + list + + For instance, if you want to filter edges with a numeric C{weight} + property larger than 50, you have to write: + + >>> g.es.select(weight_gt=50) #doctest: +SKIP + + Similarly, to filter edges whose C{type} is in a list of predefined + types: + + >>> list_of_types = ["inhibitory", "excitatory"] + >>> g.es.select(type_in=list_of_types) #doctest: +SKIP + + If the operator is omitted, it defaults to C{eq}. For instance, the + following selector selects edges whose C{type} property is + C{intracluster}: + + >>> g.es.select(type="intracluster") #doctest: +SKIP + + In the case of an unknown operator, it is assumed that the + recognized operator is part of the attribute name and the actual + operator is C{eq}. + + Keyword arguments are treated specially if they start with an + underscore (C{_}). These are not real attributes but refer to specific + properties of the edges, e.g., their centrality. The rules are as + follows: + + 1. C{_source} or {_from} means the source vertex of an edge. For + undirected graphs, only the C{eq} operator is supported and it + is treated as {_incident} (since undirected graphs have no notion + of edge directionality). + + 2. C{_target} or {_to} means the target vertex of an edge. For + undirected graphs, only the C{eq} operator is supported and it + is treated as {_incident} (since undirected graphs have no notion + of edge directionality). + + 3. C{_within} ignores the operator and checks whether both endpoints + of the edge lie within a specified set. + + 4. C{_between} ignores the operator and checks whether I{one} + endpoint of the edge lies within a specified set and the I{other} + endpoint lies within another specified set. The two sets must be + given as a tuple. + + 5. C{_incident} ignores the operator and checks whether the edge is + incident on a specific vertex or a set of vertices. + + 6. Otherwise, the rest of the name is interpreted as a method of the + L{Graph} object. This method is called with the edge sequence as + its first argument (all others left at default values) and edges + are filtered according to the value returned by the method. + + For instance, if you want to exclude edges with a betweenness + centrality less than 2: + + >>> g = Graph.Famous("zachary") + >>> excl = g.es.select(_edge_betweenness_ge = 2) + + To select edges originating from vertices 2 and 4: + + >>> edges = g.es.select(_source_in = [2, 4]) + + To select edges lying entirely within the subgraph spanned by vertices + 2, 3, 4 and 7: + + >>> edges = g.es.select(_within = [2, 3, 4, 7]) + + To select edges with one endpoint in the vertex set containing vertices + 2, 3, 4 and 7 and the other endpoint in the vertex set containing + vertices 8 and 9: + + >>> edges = g.es.select(_between = ([2, 3, 4, 7], [8, 9])) + + For properties that take a long time to be computed (e.g., betweenness + centrality for large graphs), it is advised to calculate the values + in advance and store it in a graph attribute. The same applies when + you are selecting based on the same property more than once in the + same C{select()} call to avoid calculating it twice unnecessarily. + For instance, the following would calculate betweenness centralities + twice: + + >>> edges = g.es.select(_edge_betweenness_gt=10, # doctest:+SKIP + ... _edge_betweenness_lt=30) + + It is advised to use this instead: + + >>> g.es["bs"] = g.edge_betweenness() + >>> edges = g.es.select(bs_gt=10, bs_lt=30) + + @return: the new, filtered edge sequence + """ + es = _EdgeSeq.select(self, *args) + is_directed = self.graph.is_directed() + + def _ensure_set(value): + if isinstance(value, VertexSeq): + value = set(v.index for v in value) + elif not isinstance(value, (set, frozenset)): + value = set(value) + return value + + operators = { + "lt": operator.lt, + "gt": operator.gt, + "le": operator.le, + "ge": operator.ge, + "eq": operator.eq, + "ne": operator.ne, + "in": lambda a, b: a in b, + "notin": lambda a, b: a not in b, + } + + # TODO(ntamas): some keyword arguments should be prioritized over + # others; for instance, we have optimized code paths for _source and + # _target in directed and undirected graphs if es.is_all() is True; + # these should be executed first. This matters only if there are + # multiple keyword arguments and es.is_all() is True. + + for keyword, value in kwds.items(): + if "_" not in keyword or keyword.rindex("_") == 0: + keyword = keyword + "_eq" + pos = keyword.rindex("_") + attr, op = keyword[0:pos], keyword[pos + 1 :] + try: + func = operators[op] + except KeyError: + # No such operator, assume that it's part of the attribute name + attr, op, func = keyword, "eq", operators["eq"] + + if attr[0] == "_": + if attr in ("_source", "_from", "_target", "_to") and not is_directed: + if op not in ("eq", "in"): + raise RuntimeError("unsupported for undirected graphs") + + # translate to _incident to avoid confusion + attr = "_incident" + if func == operators["eq"]: + if hasattr(value, "__iter__") and not isinstance(value, str): + value = set(value) + else: + value = set([value]) + + if attr in ("_source", "_from"): + if es.is_all() and op == "eq": + # shortcut here: use .incident() as it is much faster + filtered_idxs = sorted(es.graph.incident(value, mode="out")) + func = None + # TODO(ntamas): there are more possibilities; we could + # optimize "ne", "in" and "notin" in similar ways + else: + values = [e.source for e in es] + if op == "in" or op == "notin": + value = _ensure_set(value) + + elif attr in ("_target", "_to"): + if es.is_all() and op == "eq": + # shortcut here: use .incident() as it is much faster + filtered_idxs = sorted(es.graph.incident(value, mode="in")) + func = None + # TODO(ntamas): there are more possibilities; we could + # optimize "ne", "in" and "notin" in similar ways + else: + values = [e.target for e in es] + if op == "in" or op == "notin": + value = _ensure_set(value) + + elif attr == "_incident": + func = None # ignoring function, filtering here + value = _ensure_set(value) + + # Fetch all the edges that are incident on at least one of + # the vertices specified + candidates = set() + for v in value: + candidates.update(es.graph.incident(v)) + + if not es.is_all(): + # Find those that are in the current edge sequence + filtered_idxs = [ + i for i, e in enumerate(es) if e.index in candidates + ] + else: + # We are done, the filtered indexes are in the candidates set + filtered_idxs = sorted(candidates) + + elif attr == "_within": + func = None # ignoring function, filtering here + value = _ensure_set(value) + + # Fetch all the edges that are incident on at least one of + # the vertices specified + candidates = set() + for v in value: + candidates.update(es.graph.incident(v)) + + if not es.is_all(): + # Find those where both endpoints are OK + filtered_idxs = [ + i + for i, e in enumerate(es) + if e.index in candidates + and e.source in value + and e.target in value + ] + else: + # Optimized version when the edge sequence contains all + # the edges exactly once in increasing order of edge IDs + filtered_idxs = [ + i + for i in candidates + if es[i].source in value and es[i].target in value + ] + + elif attr == "_between": + if len(value) != 2: + raise ValueError( + "_between selector requires two vertex ID lists" + ) + func = None # ignoring function, filtering here + set1 = _ensure_set(value[0]) + set2 = _ensure_set(value[1]) + + # Fetch all the edges that are incident on at least one of + # the vertices specified + candidates = set() + for v in set1: + candidates.update(es.graph.incident(v)) + for v in set2: + candidates.update(es.graph.incident(v)) + + if not es.is_all(): + # Find those where both endpoints are OK + filtered_idxs = [ + i + for i, e in enumerate(es) + if (e.source in set1 and e.target in set2) + or (e.target in set1 and e.source in set2) + ] + else: + # Optimized version when the edge sequence contains all + # the edges exactly once in increasing order of edge IDs + filtered_idxs = [ + i + for i in candidates + if (es[i].source in set1 and es[i].target in set2) + or (es[i].target in set1 and es[i].source in set2) + ] + + else: + # Method call, not an attribute + values = getattr(es.graph, attr[1:])(es) + else: + values = es[attr] + + # If we have a function to apply on the values, do that; otherwise + # we assume that filtered_idxs has already been calculated. + if func is not None: + filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] + + es = es.select(filtered_idxs) + + return es + + def __call__(self, *args, **kwds): + """Shorthand notation to select() + + This method simply passes all its arguments to L{EdgeSeq.select()}. + """ + return self.select(*args, **kwds) + + From b1523554603454409743584486ddcd367c5004ea Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 2 Oct 2021 15:42:32 +1000 Subject: [PATCH 0469/1681] Rename self -> graph to make it a little more readable --- src/igraph/operators/methods.py | 76 ++++++++++++++++----------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/igraph/operators/methods.py b/src/igraph/operators/methods.py index 86ccd2821..9024111f2 100644 --- a/src/igraph/operators/methods.py +++ b/src/igraph/operators/methods.py @@ -16,30 +16,30 @@ ) -def __iadd__(self, other): +def __iadd__(graph, other): """In-place addition (disjoint union). @see: L{__add__} """ if isinstance(other, (int, str)): - self.add_vertices(other) - return self + graph.add_vertices(other) + return graph elif isinstance(other, tuple) and len(other) == 2: - self.add_edges([other]) - return self + graph.add_edges([other]) + return graph elif isinstance(other, list): if not other: - return self + return graph if isinstance(other[0], tuple): - self.add_edges(other) - return self + graph.add_edges(other) + return graph if isinstance(other[0], str): - self.add_vertices(other) - return self + graph.add_vertices(other) + return graph return NotImplemented -def __add__(self, other): +def __add__(graph, other): """Copies the graph and extends the copy depending on the type of the other object given. @@ -55,35 +55,35 @@ def __add__(self, other): from igraph import Graph if isinstance(other, (int, str)): - g = self.copy() + g = graph.copy() g.add_vertices(other) elif isinstance(other, tuple) and len(other) == 2: - g = self.copy() + g = graph.copy() g.add_edges([other]) elif isinstance(other, list): if len(other) > 0: if isinstance(other[0], tuple): - g = self.copy() + g = graph.copy() g.add_edges(other) elif isinstance(other[0], str): - g = self.copy() + g = graph.copy() g.add_vertices(other) elif isinstance(other[0], Graph): - return self.disjoint_union(other) + return graph.disjoint_union(other) else: return NotImplemented else: - return self.copy() + return graph.copy() elif isinstance(other, Graph): - return self.disjoint_union(other) + return graph.disjoint_union(other) else: return NotImplemented return g -def __and__(self, other): +def __and__(graph, other): """Graph intersection operator. @param other: the other graph to take the intersection with. @@ -93,41 +93,41 @@ def __and__(self, other): from igraph import Graph if isinstance(other, Graph): - return self.intersection(other) + return graph.intersection(other) else: return NotImplemented -def __isub__(self, other): +def __isub__(graph, other): """In-place subtraction (difference). @see: L{__sub__}""" if isinstance(other, int): - self.delete_vertices([other]) + graph.delete_vertices([other]) elif isinstance(other, tuple) and len(other) == 2: - self.delete_edges([other]) + graph.delete_edges([other]) elif isinstance(other, list): if len(other) > 0: if isinstance(other[0], tuple): - self.delete_edges(other) + graph.delete_edges(other) elif isinstance(other[0], (int, str)): - self.delete_vertices(other) + graph.delete_vertices(other) else: return NotImplemented elif isinstance(other, Vertex): - self.delete_vertices(other) + graph.delete_vertices(other) elif isinstance(other, VertexSeq): - self.delete_vertices(other) + graph.delete_vertices(other) elif isinstance(other, Edge): - self.delete_edges(other) + graph.delete_edges(other) elif isinstance(other, EdgeSeq): - self.delete_edges(other) + graph.delete_edges(other) else: return NotImplemented - return self + return graph -def __sub__(self, other): +def __sub__(graph, other): """Removes the given object(s) from the graph @param other: if it is an integer, removes the vertex with the given @@ -141,9 +141,9 @@ def __sub__(self, other): from igraph import Graph if isinstance(other, Graph): - return self.difference(other) + return graph.difference(other) - result = self.copy() + result = graph.copy() if isinstance(other, (int, str)): result.delete_vertices([other]) elif isinstance(other, tuple) and len(other) == 2: @@ -172,7 +172,7 @@ def __sub__(self, other): return result -def __mul__(self, other): +def __mul__(graph, other): """Copies exact replicas of the original graph an arbitrary number of times. @@ -187,16 +187,16 @@ def __mul__(self, other): if other == 0: return Graph() elif other == 1: - return self + return graph elif other > 1: - return self.disjoint_union([self] * (other - 1)) + return graph.disjoint_union([graph] * (other - 1)) else: return NotImplemented return NotImplemented -def __or__(self, other): +def __or__(graph, other): """Graph union operator. @param other: the other graph to take the union with. @@ -206,6 +206,6 @@ def __or__(self, other): from igraph import Graph if isinstance(other, Graph): - return self.union(other) + return graph.union(other) else: return NotImplemented From c29061e8bdc260ba32f7c69b031371f4451d9165 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 2 Oct 2021 20:22:26 +1000 Subject: [PATCH 0470/1681] Basic add/delete methods --- src/igraph/__init__.py | 173 ++++++----------------------------- src/igraph/basic/__init__.py | 144 +++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 143 deletions(-) create mode 100644 src/igraph/basic/__init__.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 974822571..d35c93320 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -75,6 +75,13 @@ set_status_handler, __igraph_version__, ) +from igraph.basic import ( + _add_edge, + _add_edges, + _add_vertex, + _add_vertices, + _delete_edges, +) from igraph.community import ( _community_fastgreedy, _community_infomap, @@ -409,113 +416,6 @@ def __init__(self, *args, **kwds): key = str(key) self.es[key] = value - def add_edge(self, source, target, **kwds): - """Adds a single edge to the graph. - - Keyword arguments (except the source and target arguments) will be - assigned to the edge as attributes. - - The performance cost of adding a single edge or several edges - to a graph is similar. Thus, when adding several edges, a single - C{add_edges()} call is more efficient than multiple C{add_edge()} calls. - - @param source: the source vertex of the edge or its name. - @param target: the target vertex of the edge or its name. - - @return: the newly added edge as an L{Edge} object. Use - C{add_edges([(source, target)])} if you don't need the L{Edge} - object and want to avoid the overhead of creating it. - """ - eid = self.ecount() - self.add_edges([(source, target)]) - edge = self.es[eid] - for key, value in kwds.items(): - edge[key] = value - return edge - - def add_edges(self, es, attributes=None): - """Adds some edges to the graph. - - @param es: the list of edges to be added. Every edge is represented - with a tuple containing the vertex IDs or names of the two - endpoints. Vertices are enumerated from zero. - @param attributes: dict of sequences, all of length equal to the - number of edges to be added, containing the attributes of the new - edges. - """ - eid = self.ecount() - res = GraphBase.add_edges(self, es) - n = self.ecount() - eid - if (attributes is not None) and (n > 0): - for key, val in list(attributes.items()): - self.es[eid:][key] = val - return res - - def add_vertex(self, name=None, **kwds): - """Adds a single vertex to the graph. Keyword arguments will be assigned - as vertex attributes. Note that C{name} as a keyword argument is treated - specially; if a graph has C{name} as a vertex attribute, it allows one - to refer to vertices by their names in most places where igraph expects - a vertex ID. - - @return: the newly added vertex as a L{Vertex} object. Use - C{add_vertices(1)} if you don't need the L{Vertex} object and want - to avoid the overhead of creating t. - """ - vid = self.vcount() - self.add_vertices(1) - vertex = self.vs[vid] - for key, value in kwds.items(): - vertex[key] = value - if name is not None: - vertex["name"] = name - return vertex - - def add_vertices(self, n, attributes=None): - """Adds some vertices to the graph. - - Note that if C{n} is a sequence of strings, indicating the names of the - new vertices, and attributes has a key C{name}, the two conflict. In - that case the attribute will be applied. - - @param n: the number of vertices to be added, or the name of a single - vertex to be added, or a sequence of strings, each corresponding to the - name of a vertex to be added. Names will be assigned to the C{name} - vertex attribute. - @param attributes: dict of sequences, all of length equal to the - number of vertices to be added, containing the attributes of the new - vertices. If n is a string (so a single vertex is added), then the - values of this dict are the attributes themselves, but if n=1 then - they have to be lists of length 1. - """ - if isinstance(n, str): - # Adding a single vertex with a name - m = self.vcount() - result = GraphBase.add_vertices(self, 1) - self.vs[m]["name"] = n - if attributes is not None: - for key, val in list(attributes.items()): - self.vs[m][key] = val - elif hasattr(n, "__iter__"): - m = self.vcount() - if not hasattr(n, "__len__"): - names = list(n) - else: - names = n - result = GraphBase.add_vertices(self, len(names)) - if len(names) > 0: - self.vs[m:]["name"] = names - if attributes is not None: - for key, val in list(attributes.items()): - self.vs[m:][key] = val - else: - result = GraphBase.add_vertices(self, n) - if (attributes is not None) and (n > 0): - m = self.vcount() - n - for key, val in list(attributes.items()): - self.vs[m:][key] = val - return result - def as_directed(self, *args, **kwds): """Returns a directed copy of this graph. Arguments are passed on to L{to_directed()} that is invoked on the copy. @@ -532,36 +432,27 @@ def as_undirected(self, *args, **kwds): copy.to_undirected(*args, **kwds) return copy - def delete_edges(self, *args, **kwds): - """Deletes some edges from the graph. - - The set of edges to be deleted is determined by the positional and - keyword arguments. If the function is called without any arguments, - all edges are deleted. If any keyword argument is present, or the - first positional argument is callable, an edge sequence is derived by - calling L{EdgeSeq.select} with the same positional and keyword - arguments. Edges in the derived edge sequence will be removed. - Otherwise the first positional argument is considered as follows: - - - C{None} - deletes all edges (deprecated since 0.8.3) - - a single integer - deletes the edge with the given ID - - a list of integers - deletes the edges denoted by the given IDs - - a list of 2-tuples - deletes the edges denoted by the given - source-target vertex pairs. When multiple edges are present - between a given source-target vertex pair, only one is removed. - - @deprecated: C{delete_edges(None)} has been replaced by - C{delete_edges()} - with no arguments - since igraph 0.8.3. + def clear(self): + """Clears the graph, deleting all vertices, edges, and attributes. + + @see: L{delete_vertices} and L{delete_edges}. """ - if len(args) == 0 and len(kwds) == 0: - return GraphBase.delete_edges(self) + self.delete_vertices() + for attr in self.attributes(): + del self[attr] - if len(kwds) > 0 or (callable(args[0]) and not isinstance(args[0], EdgeSeq)): - edge_seq = self.es(*args, **kwds) - else: - edge_seq = args[0] - return GraphBase.delete_edges(self, edge_seq) + # Basic operations + add_edge = _add_edge + + add_edges = _add_edges + add_vertex = _add_vertex + + add_vertices = _add_vertices + + delete_edges = _delete_edges + + # Structural properties def indegree(self, *args, **kwds): """Returns the in-degrees in a list. @@ -654,15 +545,6 @@ def biconnected_components(self, return_articulation_points=False): blocks = biconnected_components - def clear(self): - """Clears the graph, deleting all vertices, edges, and attributes. - - @see: L{delete_vertices} and L{delete_edges}. - """ - self.delete_vertices() - for attr in self.attributes(): - del self[attr] - def cohesive_blocks(self): """Calculates the cohesive block structure of the graph. @@ -2222,4 +2104,9 @@ def write(graph, filename, *args, **kwds): _community_leiden, _graph_plot, _operator_method_registry, + _add_edge, + _add_edges, + _add_vertex, + _add_vertices, + _delete_edges, ) diff --git a/src/igraph/basic/__init__.py b/src/igraph/basic/__init__.py new file mode 100644 index 000000000..7b7df7187 --- /dev/null +++ b/src/igraph/basic/__init__.py @@ -0,0 +1,144 @@ +from igraph._igraph import GraphBase +from igraph.seq import EdgeSeq + + +def _add_edge(graph, source, target, **kwds): + """Adds a single edge to the graph. + + Keyword arguments (except the source and target arguments) will be + assigned to the edge as attributes. + + The performance cost of adding a single edge or several edges + to a graph is similar. Thus, when adding several edges, a single + C{add_edges()} call is more efficient than multiple C{add_edge()} calls. + + @param source: the source vertex of the edge or its name. + @param target: the target vertex of the edge or its name. + + @return: the newly added edge as an L{Edge} object. Use + C{add_edges([(source, target)])} if you don't need the L{Edge} + object and want to avoid the overhead of creating it. + """ + eid = graph.ecount() + graph.add_edges([(source, target)]) + edge = graph.es[eid] + for key, value in kwds.items(): + edge[key] = value + return edge + + +def _add_edges(graph, es, attributes=None): + """Adds some edges to the graph. + + @param es: the list of edges to be added. Every edge is represented + with a tuple containing the vertex IDs or names of the two + endpoints. Vertices are enumerated from zero. + @param attributes: dict of sequences, all of length equal to the + number of edges to be added, containing the attributes of the new + edges. + """ + eid = graph.ecount() + res = GraphBase.add_edges(graph, es) + n = graph.ecount() - eid + if (attributes is not None) and (n > 0): + for key, val in list(attributes.items()): + graph.es[eid:][key] = val + return res + + +def _add_vertex(graph, name=None, **kwds): + """Adds a single vertex to the graph. Keyword arguments will be assigned + as vertex attributes. Note that C{name} as a keyword argument is treated + specially; if a graph has C{name} as a vertex attribute, it allows one + to refer to vertices by their names in most places where igraph expects + a vertex ID. + + @return: the newly added vertex as a L{Vertex} object. Use + C{add_vertices(1)} if you don't need the L{Vertex} object and want + to avoid the overhead of creating t. + """ + vid = graph.vcount() + graph.add_vertices(1) + vertex = graph.vs[vid] + for key, value in kwds.items(): + vertex[key] = value + if name is not None: + vertex["name"] = name + return vertex + + +def _add_vertices(graph, n, attributes=None): + """Adds some vertices to the graph. + + Note that if C{n} is a sequence of strings, indicating the names of the + new vertices, and attributes has a key C{name}, the two conflict. In + that case the attribute will be applied. + + @param n: the number of vertices to be added, or the name of a single + vertex to be added, or a sequence of strings, each corresponding to the + name of a vertex to be added. Names will be assigned to the C{name} + vertex attribute. + @param attributes: dict of sequences, all of length equal to the + number of vertices to be added, containing the attributes of the new + vertices. If n is a string (so a single vertex is added), then the + values of this dict are the attributes themselves, but if n=1 then + they have to be lists of length 1. + """ + if isinstance(n, str): + # Adding a single vertex with a name + m = graph.vcount() + result = GraphBase.add_vertices(graph, 1) + graph.vs[m]["name"] = n + if attributes is not None: + for key, val in list(attributes.items()): + graph.vs[m][key] = val + elif hasattr(n, "__iter__"): + m = graph.vcount() + if not hasattr(n, "__len__"): + names = list(n) + else: + names = n + result = GraphBase.add_vertices(graph, len(names)) + if len(names) > 0: + graph.vs[m:]["name"] = names + if attributes is not None: + for key, val in list(attributes.items()): + graph.vs[m:][key] = val + else: + result = GraphBase.add_vertices(graph, n) + if (attributes is not None) and (n > 0): + m = graph.vcount() - n + for key, val in list(attributes.items()): + graph.vs[m:][key] = val + return result + + +def _delete_edges(graph, *args, **kwds): + """Deletes some edges from the graph. + + The set of edges to be deleted is determined by the positional and + keyword arguments. If the function is called without any arguments, + all edges are deleted. If any keyword argument is present, or the + first positional argument is callable, an edge sequence is derived by + calling L{EdgeSeq.select} with the same positional and keyword + arguments. Edges in the derived edge sequence will be removed. + Otherwise the first positional argument is considered as follows: + + - C{None} - deletes all edges (deprecated since 0.8.3) + - a single integer - deletes the edge with the given ID + - a list of integers - deletes the edges denoted by the given IDs + - a list of 2-tuples - deletes the edges denoted by the given + source-target vertex pairs. When multiple edges are present + between a given source-target vertex pair, only one is removed. + + @deprecated: C{delete_edges(None)} has been replaced by + C{delete_edges()} - with no arguments - since igraph 0.8.3. + """ + if len(args) == 0 and len(kwds) == 0: + return GraphBase.delete_edges(graph) + + if len(kwds) > 0 or (callable(args[0]) and not isinstance(args[0], EdgeSeq)): + edge_seq = graph.es(*args, **kwds) + else: + edge_seq = args[0] + return GraphBase.delete_edges(graph, edge_seq) From 6f6accd2a93ce561cae26072da6f8572ee4ff164 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Sat, 2 Oct 2021 20:30:19 +1000 Subject: [PATCH 0471/1681] More operators --- src/igraph/__init__.py | 41 ++++------------------------ src/igraph/operators/__init__.py | 6 ++++ src/igraph/operators/methods.py | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index d35c93320..15ace8548 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1640,6 +1640,12 @@ def _as_parameter_(self): __or__ = _operator_method_registry['__or__'] + disjoint_union = _operator_method_registry['disjoint_union'] + + union = _operator_method_registry['union'] + + intersection = _operator_method_registry['intersection'] + def __bool__(self): """Returns True if the graph has at least one vertex, False otherwise.""" return self.vcount() > 0 @@ -1736,41 +1742,6 @@ def summary(self, verbosity=0, width=None, *args, **kwds): """ return str(GraphSummary(self, verbosity, width, *args, **kwds)) - def disjoint_union(self, other): - """Creates the disjoint union of two (or more) graphs. - - @param other: graph or list of graphs to be united with the current one. - @return: the disjoint union graph - """ - if isinstance(other, GraphBase): - other = [other] - return disjoint_union([self] + other) - - def union(self, other, byname="auto"): - """Creates the union of two (or more) graphs. - - @param other: graph or list of graphs to be united with the current one. - @param byname: whether to use vertex names instead of ids. See - L{igraph.union} for details. - @return: the union graph - """ - if isinstance(other, GraphBase): - other = [other] - return union([self] + other, byname=byname) - - def intersection(self, other, byname="auto"): - """Creates the intersection of two (or more) graphs. - - @param other: graph or list of graphs to be intersected with - the current one. - @param byname: whether to use vertex names instead of ids. See - L{igraph.intersection} for details. - @return: the intersection graph - """ - if isinstance(other, GraphBase): - other = [other] - return intersection([self] + other, byname=byname) - _format_mapping = { "ncol": ("Read_Ncol", "write_ncol"), "lgl": ("Read_Lgl", "write_lgl"), diff --git a/src/igraph/operators/__init__.py b/src/igraph/operators/__init__.py index cb3407a4c..0b6e86291 100644 --- a/src/igraph/operators/__init__.py +++ b/src/igraph/operators/__init__.py @@ -22,6 +22,9 @@ __sub__, __mul__, __or__, + _disjoint_union, + _union, + _intersection, ) operator_method_registry = { @@ -32,4 +35,7 @@ '__sub__': __sub__, '__mul__': __mul__, '__or__': __or__, + 'disjoint_union': _disjoint_union, + 'union': _union, + 'intersection': _intersection, } diff --git a/src/igraph/operators/methods.py b/src/igraph/operators/methods.py index 9024111f2..1822103d9 100644 --- a/src/igraph/operators/methods.py +++ b/src/igraph/operators/methods.py @@ -1,8 +1,14 @@ from igraph._igraph import ( + GraphBase, Vertex, Edge, ) from igraph.seq import VertexSeq, EdgeSeq +from igraph.operators.functions import ( + disjoint_union, + union, + intersection, +) __all__ = ( @@ -13,9 +19,50 @@ "__sub__", "__mul__", "__or__", + "_disjoint_union", + "_union", + "_intersection", ) +def _disjoint_union(graph, other): + """Creates the disjoint union of two (or more) graphs. + + @param other: graph or list of graphs to be united with the current one. + @return: the disjoint union graph + """ + if isinstance(other, GraphBase): + other = [other] + return disjoint_union([graph] + other) + + +def _union(graph, other, byname="auto"): + """Creates the union of two (or more) graphs. + + @param other: graph or list of graphs to be united with the current one. + @param byname: whether to use vertex names instead of ids. See + L{igraph.union} for details. + @return: the union graph + """ + if isinstance(other, GraphBase): + other = [other] + return union([graph] + other, byname=byname) + + +def _intersection(graph, other, byname="auto"): + """Creates the intersection of two (or more) graphs. + + @param other: graph or list of graphs to be intersected with + the current one. + @param byname: whether to use vertex names instead of ids. See + L{igraph.intersection} for details. + @return: the intersection graph + """ + if isinstance(other, GraphBase): + other = [other] + return intersection([graph] + other, byname=byname) + + def __iadd__(graph, other): """In-place addition (disjoint union). From 72074a17676fe06a3d9c2c1c6fe50a11b04b3a3f Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 5 Oct 2021 07:37:50 +1100 Subject: [PATCH 0472/1681] More layout functions/methods --- src/igraph/__init__.py | 290 +++------------------------------------- src/igraph/layout.py | 292 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+), 274 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 15ace8548..6885653a7 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -190,7 +190,14 @@ _construct_random_bipartite_graph, ) from igraph.io.images import _write_graph_to_svg -from igraph.layout import Layout +from igraph.layout import ( + Layout, + _layout, + _layout_auto, + _layout_sugiyama, + _layout_method_wrapper, + _3d_version_for, +) from igraph.matching import Matching from igraph.operators import ( disjoint_union, @@ -1123,238 +1130,11 @@ def get_automorphisms_vf2( community_leiden = _community_leiden - def layout(self, layout=None, *args, **kwds): - """Returns the layout of the graph according to a layout algorithm. - - Parameters and keyword arguments not specified here are passed to the - layout algorithm directly. See the documentation of the layout - algorithms for the explanation of these parameters. - - Registered layout names understood by this method are: - - - C{auto}, C{automatic}: automatic layout - (see L{layout_auto}) - - - C{bipartite}: bipartite layout (see L{layout_bipartite}) - - - C{circle}, C{circular}: circular layout - (see L{layout_circle}) - - - C{dh}, C{davidson_harel}: Davidson-Harel layout (see - L{layout_davidson_harel}) - - - C{drl}: DrL layout for large graphs (see L{layout_drl}) - - - C{drl_3d}: 3D DrL layout for large graphs - (see L{layout_drl}) - - - C{fr}, C{fruchterman_reingold}: Fruchterman-Reingold layout - (see L{layout_fruchterman_reingold}). - - - C{fr_3d}, C{fr3d}, C{fruchterman_reingold_3d}: 3D Fruchterman- - Reingold layout (see L{layout_fruchterman_reingold}). - - - C{grid}: regular grid layout in 2D (see L{layout_grid}) - - - C{grid_3d}: regular grid layout in 3D (see L{layout_grid_3d}) - - - C{graphopt}: the graphopt algorithm (see L{layout_graphopt}) - - - C{kk}, C{kamada_kawai}: Kamada-Kawai layout - (see L{layout_kamada_kawai}) + layout = _layout - - C{kk_3d}, C{kk3d}, C{kamada_kawai_3d}: 3D Kamada-Kawai layout - (see L{layout_kamada_kawai}) + layout_auto = _layout_auto - - C{lgl}, C{large}, C{large_graph}: Large Graph Layout - (see L{layout_lgl}) - - - C{mds}: multidimensional scaling layout (see L{layout_mds}) - - - C{random}: random layout (see L{layout_random}) - - - C{random_3d}: random 3D layout (see L{layout_random}) - - - C{rt}, C{tree}, C{reingold_tilford}: Reingold-Tilford tree - layout (see L{layout_reingold_tilford}) - - - C{rt_circular}, C{reingold_tilford_circular}: circular - Reingold-Tilford tree layout - (see L{layout_reingold_tilford_circular}) - - - C{sphere}, C{spherical}, C{circle_3d}, C{circular_3d}: spherical - layout (see L{layout_circle}) - - - C{star}: star layout (see L{layout_star}) - - - C{sugiyama}: Sugiyama layout (see L{layout_sugiyama}) - - @param layout: the layout to use. This can be one of the registered - layout names or a callable which returns either a L{Layout} object or - a list of lists containing the coordinates. If C{None}, uses the - value of the C{plotting.layout} configuration key. - @return: a L{Layout} object. - """ - if layout is None: - layout = config["plotting.layout"] - if hasattr(layout, "__call__"): - method = layout - else: - layout = layout.lower() - if layout[-3:] == "_3d": - kwds["dim"] = 3 - layout = layout[:-3] - elif layout[-2:] == "3d": - kwds["dim"] = 3 - layout = layout[:-2] - method = getattr(self.__class__, self._layout_mapping[layout]) - if not hasattr(method, "__call__"): - raise ValueError("layout method must be callable") - layout = method(self, *args, **kwds) - if not isinstance(layout, Layout): - layout = Layout(layout) - return layout - - def layout_auto(self, *args, **kwds): - """Chooses and runs a suitable layout function based on simple - topological properties of the graph. - - This function tries to choose an appropriate layout function for - the graph using the following rules: - - 1. If the graph has an attribute called C{layout}, it will be - used. It may either be a L{Layout} instance, a list of - coordinate pairs, the name of a layout function, or a - callable function which generates the layout when called - with the graph as a parameter. - - 2. Otherwise, if the graph has vertex attributes called C{x} - and C{y}, these will be used as coordinates in the layout. - When a 3D layout is requested (by setting C{dim} to 3), - a vertex attribute named C{z} will also be needed. - - 3. Otherwise, if the graph is connected and has at most 100 - vertices, the Kamada-Kawai layout will be used (see - L{layout_kamada_kawai()}). - - 4. Otherwise, if the graph has at most 1000 vertices, the - Fruchterman-Reingold layout will be used (see - L{layout_fruchterman_reingold()}). - - 5. If everything else above failed, the DrL layout algorithm - will be used (see L{layout_drl()}). - - All the arguments of this function except C{dim} are passed on - to the chosen layout function (in case we have to call some layout - function). - - @keyword dim: specifies whether we would like to obtain a 2D or a - 3D layout. - @return: a L{Layout} object. - """ - if "layout" in self.attributes(): - layout = self["layout"] - if isinstance(layout, Layout): - # Layouts are used intact - return layout - if isinstance(layout, (list, tuple)): - # Lists/tuples are converted to layouts - return Layout(layout) - if hasattr(layout, "__call__"): - # Callables are called - return Layout(layout(*args, **kwds)) - # Try Graph.layout() - return self.layout(layout, *args, **kwds) - - dim = kwds.get("dim", 2) - vattrs = self.vertex_attributes() - if "x" in vattrs and "y" in vattrs: - if dim == 3 and "z" in vattrs: - return Layout(list(zip(self.vs["x"], self.vs["y"], self.vs["z"]))) - else: - return Layout(list(zip(self.vs["x"], self.vs["y"]))) - - if self.vcount() <= 100 and self.is_connected(): - algo = "kk" - elif self.vcount() <= 1000: - algo = "fr" - else: - algo = "drl" - return self.layout(algo, *args, **kwds) - - def layout_sugiyama( - self, - layers=None, - weights=None, - hgap=1, - vgap=1, - maxiter=100, - return_extended_graph=False, - ): - """Places the vertices using a layered Sugiyama layout. - - This is a layered layout that is most suitable for directed acyclic graphs, - although it works on undirected or cyclic graphs as well. - - Each vertex is assigned to a layer and each layer is placed on a horizontal - line. Vertices within the same layer are then permuted using the barycenter - heuristic that tries to minimize edge crossings. - - Dummy vertices will be added on edges that span more than one layer. The - returned layout therefore contains more rows than the number of nodes in - the original graph; the extra rows correspond to the dummy vertices. - - @param layers: a vector specifying a non-negative integer layer index for - each vertex, or the name of a numeric vertex attribute that contains - the layer indices. If C{None}, a layering will be determined - automatically. For undirected graphs, a spanning tree will be extracted - and vertices will be assigned to layers using a breadth first search from - the node with the largest degree. For directed graphs, cycles are broken - by reversing the direction of edges in an approximate feedback arc set - using the heuristic of Eades, Lin and Smyth, and then using longest path - layering to place the vertices in layers. - @param weights: edge weights to be used. Can be a sequence or iterable or - even an edge attribute name. - @param hgap: minimum horizontal gap between vertices in the same layer. - @param vgap: vertical gap between layers. The layer index will be - multiplied by I{vgap} to obtain the Y coordinate. - @param maxiter: maximum number of iterations to take in the crossing - reduction step. Increase this if you feel that you are getting too many - edge crossings. - @param return_extended_graph: specifies that the extended graph with the - added dummy vertices should also be returned. When this is C{True}, the - result will be a tuple containing the layout and the extended graph. The - first |V| nodes of the extended graph will correspond to the nodes of the - original graph, the remaining ones are dummy nodes. Plotting the extended - graph with the returned layout and hidden dummy nodes will produce a layout - that is similar to the original graph, but with the added edge bends. - The extended graph also contains an edge attribute called C{_original_eid} - which specifies the ID of the edge in the original graph from which the - edge of the extended graph was created. - @return: the calculated layout, which may (and usually will) have more rows - than the number of vertices; the remaining rows correspond to the dummy - nodes introduced in the layering step. When C{return_extended_graph} is - C{True}, it will also contain the extended graph. - - @newfield ref: Reference - @ref: K Sugiyama, S Tagawa, M Toda: Methods for visual understanding of - hierarchical system structures. IEEE Systems, Man and Cybernetics\ - 11(2):109-125, 1981. - @ref: P Eades, X Lin and WF Smyth: A fast effective heuristic for the - feedback arc set problem. Information Processing Letters 47:319-323, 1993. - """ - if not return_extended_graph: - return Layout( - GraphBase._layout_sugiyama( - self, layers, weights, hgap, vgap, maxiter, return_extended_graph - ) - ) - - layout, extd_graph, extd_to_orig_eids = GraphBase._layout_sugiyama( - self, layers, weights, hgap, vgap, maxiter, return_extended_graph - ) - extd_graph.es["_original_eid"] = extd_to_orig_eids - return Layout(layout), extd_graph + layout_sugiyama = _layout_sugiyama def maximum_bipartite_matching(self, types="type", weights=None, eps=None): """Finds a maximum matching in a bipartite graph. @@ -1914,26 +1694,6 @@ def _add_proxy_methods(): ############################################################## # Making sure that layout methods always return a Layout - -def _layout_method_wrapper(func): - """Wraps an existing layout method to ensure that it returns a Layout - instead of a list of lists. - - @param func: the method to wrap. Must be a method of the Graph object. - @return: a new method - """ - - def result(*args, **kwds): - layout = func(*args, **kwds) - if not isinstance(layout, Layout): - layout = Layout(layout) - return layout - - result.__name__ = func.__name__ - result.__doc__ = func.__doc__ - return result - - for name in dir(Graph): if not name.startswith("layout_"): continue @@ -1944,29 +1704,6 @@ def result(*args, **kwds): ############################################################## # Adding aliases for the 3D versions of the layout methods - -def _3d_version_for(func): - """Creates an alias for the 3D version of the given layout algoritm. - - This function is a decorator that creates a method which calls I{func} after - attaching C{dim=3} to the list of keyword arguments. - - @param func: must be a method of the Graph object. - @return: a new method - """ - - def result(*args, **kwds): - kwds["dim"] = 3 - return func(*args, **kwds) - - result.__name__ = "%s_3d" % func.__name__ - result.__doc__ = """Alias for L{%s()} with dim=3.\n\n@see: Graph.%s()""" % ( - func.__name__, - func.__name__, - ) - return result - - Graph.layout_fruchterman_reingold_3d = _3d_version_for( Graph.layout_fruchterman_reingold ) @@ -2080,4 +1817,9 @@ def write(graph, filename, *args, **kwds): _add_vertex, _add_vertices, _delete_edges, + _layout, + _layout_auto, + _layout_sugiyama, + _layout_method_wrapper, + _3d_version_for, ) diff --git a/src/igraph/layout.py b/src/igraph/layout.py index d6a45814e..3aaacdc49 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -9,10 +9,21 @@ from math import sin, cos, pi +from igraph._igraph import GraphBase from igraph.drawing.utils import BoundingBox from igraph.statistics import RunningMean +__all__ = ( + 'Layout', + '_layout', + '_layout_auto', + '_layout_sugiyama', + '_layout_method_wrapper', + '_3d_version_for', +) + + class Layout: """Represents the layout of a graph. @@ -424,3 +435,284 @@ def fit_into(self, bbox, keep_aspect_ratio=True): self.scale(*ratios) self.translate(*translations) + + +def _layout(self, layout=None, *args, **kwds): + """Returns the layout of the graph according to a layout algorithm. + + Parameters and keyword arguments not specified here are passed to the + layout algorithm directly. See the documentation of the layout + algorithms for the explanation of these parameters. + + Registered layout names understood by this method are: + + - C{auto}, C{automatic}: automatic layout + (see L{layout_auto}) + + - C{bipartite}: bipartite layout (see L{layout_bipartite}) + + - C{circle}, C{circular}: circular layout + (see L{layout_circle}) + + - C{dh}, C{davidson_harel}: Davidson-Harel layout (see + L{layout_davidson_harel}) + + - C{drl}: DrL layout for large graphs (see L{layout_drl}) + + - C{drl_3d}: 3D DrL layout for large graphs + (see L{layout_drl}) + + - C{fr}, C{fruchterman_reingold}: Fruchterman-Reingold layout + (see L{layout_fruchterman_reingold}). + + - C{fr_3d}, C{fr3d}, C{fruchterman_reingold_3d}: 3D Fruchterman- + Reingold layout (see L{layout_fruchterman_reingold}). + + - C{grid}: regular grid layout in 2D (see L{layout_grid}) + + - C{grid_3d}: regular grid layout in 3D (see L{layout_grid_3d}) + + - C{graphopt}: the graphopt algorithm (see L{layout_graphopt}) + + - C{kk}, C{kamada_kawai}: Kamada-Kawai layout + (see L{layout_kamada_kawai}) + + - C{kk_3d}, C{kk3d}, C{kamada_kawai_3d}: 3D Kamada-Kawai layout + (see L{layout_kamada_kawai}) + + - C{lgl}, C{large}, C{large_graph}: Large Graph Layout + (see L{layout_lgl}) + + - C{mds}: multidimensional scaling layout (see L{layout_mds}) + + - C{random}: random layout (see L{layout_random}) + + - C{random_3d}: random 3D layout (see L{layout_random}) + + - C{rt}, C{tree}, C{reingold_tilford}: Reingold-Tilford tree + layout (see L{layout_reingold_tilford}) + + - C{rt_circular}, C{reingold_tilford_circular}: circular + Reingold-Tilford tree layout + (see L{layout_reingold_tilford_circular}) + + - C{sphere}, C{spherical}, C{circle_3d}, C{circular_3d}: spherical + layout (see L{layout_circle}) + + - C{star}: star layout (see L{layout_star}) + + - C{sugiyama}: Sugiyama layout (see L{layout_sugiyama}) + + @param layout: the layout to use. This can be one of the registered + layout names or a callable which returns either a L{Layout} object or + a list of lists containing the coordinates. If C{None}, uses the + value of the C{plotting.layout} configuration key. + @return: a L{Layout} object. + """ + # Deferred import to avoid cycles + from igraph import config + + if layout is None: + layout = config["plotting.layout"] + if hasattr(layout, "__call__"): + method = layout + else: + layout = layout.lower() + if layout[-3:] == "_3d": + kwds["dim"] = 3 + layout = layout[:-3] + elif layout[-2:] == "3d": + kwds["dim"] = 3 + layout = layout[:-2] + method = getattr(self.__class__, self._layout_mapping[layout]) + if not hasattr(method, "__call__"): + raise ValueError("layout method must be callable") + layout = method(self, *args, **kwds) + if not isinstance(layout, Layout): + layout = Layout(layout) + return layout + + +def _layout_auto(self, *args, **kwds): + """Chooses and runs a suitable layout function based on simple + topological properties of the graph. + + This function tries to choose an appropriate layout function for + the graph using the following rules: + + 1. If the graph has an attribute called C{layout}, it will be + used. It may either be a L{Layout} instance, a list of + coordinate pairs, the name of a layout function, or a + callable function which generates the layout when called + with the graph as a parameter. + + 2. Otherwise, if the graph has vertex attributes called C{x} + and C{y}, these will be used as coordinates in the layout. + When a 3D layout is requested (by setting C{dim} to 3), + a vertex attribute named C{z} will also be needed. + + 3. Otherwise, if the graph is connected and has at most 100 + vertices, the Kamada-Kawai layout will be used (see + L{layout_kamada_kawai()}). + + 4. Otherwise, if the graph has at most 1000 vertices, the + Fruchterman-Reingold layout will be used (see + L{layout_fruchterman_reingold()}). + + 5. If everything else above failed, the DrL layout algorithm + will be used (see L{layout_drl()}). + + All the arguments of this function except C{dim} are passed on + to the chosen layout function (in case we have to call some layout + function). + + @keyword dim: specifies whether we would like to obtain a 2D or a + 3D layout. + @return: a L{Layout} object. + """ + if "layout" in self.attributes(): + layout = self["layout"] + if isinstance(layout, Layout): + # Layouts are used intact + return layout + if isinstance(layout, (list, tuple)): + # Lists/tuples are converted to layouts + return Layout(layout) + if hasattr(layout, "__call__"): + # Callables are called + return Layout(layout(*args, **kwds)) + # Try Graph.layout() + return self.layout(layout, *args, **kwds) + + dim = kwds.get("dim", 2) + vattrs = self.vertex_attributes() + if "x" in vattrs and "y" in vattrs: + if dim == 3 and "z" in vattrs: + return Layout(list(zip(self.vs["x"], self.vs["y"], self.vs["z"]))) + else: + return Layout(list(zip(self.vs["x"], self.vs["y"]))) + + if self.vcount() <= 100 and self.is_connected(): + algo = "kk" + elif self.vcount() <= 1000: + algo = "fr" + else: + algo = "drl" + return self.layout(algo, *args, **kwds) + + +def _layout_sugiyama( + self, + layers=None, + weights=None, + hgap=1, + vgap=1, + maxiter=100, + return_extended_graph=False, +): + """Places the vertices using a layered Sugiyama layout. + + This is a layered layout that is most suitable for directed acyclic graphs, + although it works on undirected or cyclic graphs as well. + + Each vertex is assigned to a layer and each layer is placed on a horizontal + line. Vertices within the same layer are then permuted using the barycenter + heuristic that tries to minimize edge crossings. + + Dummy vertices will be added on edges that span more than one layer. The + returned layout therefore contains more rows than the number of nodes in + the original graph; the extra rows correspond to the dummy vertices. + + @param layers: a vector specifying a non-negative integer layer index for + each vertex, or the name of a numeric vertex attribute that contains + the layer indices. If C{None}, a layering will be determined + automatically. For undirected graphs, a spanning tree will be extracted + and vertices will be assigned to layers using a breadth first search from + the node with the largest degree. For directed graphs, cycles are broken + by reversing the direction of edges in an approximate feedback arc set + using the heuristic of Eades, Lin and Smyth, and then using longest path + layering to place the vertices in layers. + @param weights: edge weights to be used. Can be a sequence or iterable or + even an edge attribute name. + @param hgap: minimum horizontal gap between vertices in the same layer. + @param vgap: vertical gap between layers. The layer index will be + multiplied by I{vgap} to obtain the Y coordinate. + @param maxiter: maximum number of iterations to take in the crossing + reduction step. Increase this if you feel that you are getting too many + edge crossings. + @param return_extended_graph: specifies that the extended graph with the + added dummy vertices should also be returned. When this is C{True}, the + result will be a tuple containing the layout and the extended graph. The + first |V| nodes of the extended graph will correspond to the nodes of the + original graph, the remaining ones are dummy nodes. Plotting the extended + graph with the returned layout and hidden dummy nodes will produce a layout + that is similar to the original graph, but with the added edge bends. + The extended graph also contains an edge attribute called C{_original_eid} + which specifies the ID of the edge in the original graph from which the + edge of the extended graph was created. + @return: the calculated layout, which may (and usually will) have more rows + than the number of vertices; the remaining rows correspond to the dummy + nodes introduced in the layering step. When C{return_extended_graph} is + C{True}, it will also contain the extended graph. + + @newfield ref: Reference + @ref: K Sugiyama, S Tagawa, M Toda: Methods for visual understanding of + hierarchical system structures. IEEE Systems, Man and Cybernetics\ + 11(2):109-125, 1981. + @ref: P Eades, X Lin and WF Smyth: A fast effective heuristic for the + feedback arc set problem. Information Processing Letters 47:319-323, 1993. + """ + if not return_extended_graph: + return Layout( + GraphBase._layout_sugiyama( + self, layers, weights, hgap, vgap, maxiter, return_extended_graph + ) + ) + + layout, extd_graph, extd_to_orig_eids = GraphBase._layout_sugiyama( + self, layers, weights, hgap, vgap, maxiter, return_extended_graph + ) + extd_graph.es["_original_eid"] = extd_to_orig_eids + return Layout(layout), extd_graph + + +def _layout_method_wrapper(func): + """Wraps an existing layout method to ensure that it returns a Layout + instead of a list of lists. + + @param func: the method to wrap. Must be a method of the Graph object. + @return: a new method + """ + + def result(*args, **kwds): + layout = func(*args, **kwds) + if not isinstance(layout, Layout): + layout = Layout(layout) + return layout + + result.__name__ = func.__name__ + result.__doc__ = func.__doc__ + return result + + +def _3d_version_for(func): + """Creates an alias for the 3D version of the given layout algoritm. + + This function is a decorator that creates a method which calls I{func} after + attaching C{dim=3} to the list of keyword arguments. + + @param func: must be a method of the Graph object. + @return: a new method + """ + + def result(*args, **kwds): + kwds["dim"] = 3 + return func(*args, **kwds) + + result.__name__ = "%s_3d" % func.__name__ + result.__doc__ = """Alias for L{%s()} with dim=3.\n\n@see: Graph.%s()""" % ( + func.__name__, + func.__name__, + ) + return result + From 9d2a947e5152836862ee81c6ff18ff534c20aaf9 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 5 Oct 2021 07:45:16 +1100 Subject: [PATCH 0473/1681] Automorphisms and fix in layout --- src/igraph/__init__.py | 51 ++++++------------------------------ src/igraph/automorphisms.py | 52 +++++++++++++++++++++++++++++++++++++ src/igraph/layout.py | 32 +++++++++++------------ 3 files changed, 76 insertions(+), 59 deletions(-) create mode 100644 src/igraph/automorphisms.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 6885653a7..e7f65ec19 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -107,6 +107,10 @@ compare_communities, split_join_distance, ) +from igraph.automorphisms import ( + _count_automorphisms_vf2, + _get_automorphisms_vf2, +) from igraph.cut import Cut, Flow from igraph.configuration import Configuration, init as init_configuration from igraph.drawing import ( @@ -1060,50 +1064,9 @@ def triad_census(self, *args, **kwds): return TriadCensus(GraphBase.triad_census(self, *args, **kwds)) # Automorphisms - def count_automorphisms_vf2( - self, color=None, edge_color=None, node_compat_fn=None, edge_compat_fn=None - ): - """Returns the number of automorphisms of the graph. - - This function simply calls C{count_isomorphisms_vf2} with the graph - itself. See C{count_isomorphisms_vf2} for an explanation of the - parameters. - - @return: the number of automorphisms of the graph - @see: Graph.count_isomorphisms_vf2 - """ - return self.count_isomorphisms_vf2( - self, - color1=color, - color2=color, - edge_color1=edge_color, - edge_color2=edge_color, - node_compat_fn=node_compat_fn, - edge_compat_fn=edge_compat_fn, - ) + count_automorphisms_vf2 = _count_automorphisms_vf2 - def get_automorphisms_vf2( - self, color=None, edge_color=None, node_compat_fn=None, edge_compat_fn=None - ): - """Returns all the automorphisms of the graph - - This function simply calls C{get_isomorphisms_vf2} with the graph - itself. See C{get_isomorphisms_vf2} for an explanation of the - parameters. - - @return: a list of lists, each item containing a possible mapping - of the graph vertices to itself according to the automorphism - @see: Graph.get_isomorphisms_vf2 - """ - return self.get_isomorphisms_vf2( - self, - color1=color, - color2=color, - edge_color1=edge_color, - edge_color2=edge_color, - node_compat_fn=node_compat_fn, - edge_compat_fn=edge_compat_fn, - ) + get_automorphisms_vf2 = _get_automorphisms_vf2 # Various clustering algorithms -- mostly wrappers around GraphBase community_fastgreedy = _community_fastgreedy @@ -1822,4 +1785,6 @@ def write(graph, filename, *args, **kwds): _layout_sugiyama, _layout_method_wrapper, _3d_version_for, + _count_automorphisms_vf2, + _get_automorphisms_vf2, ) diff --git a/src/igraph/automorphisms.py b/src/igraph/automorphisms.py new file mode 100644 index 000000000..84178e9cf --- /dev/null +++ b/src/igraph/automorphisms.py @@ -0,0 +1,52 @@ +__all__ = ( + '_count_automorphisms_vf2', + '_get_automorphisms_vf2', +) + + +def _count_automorphisms_vf2( + graph, color=None, edge_color=None, node_compat_fn=None, edge_compat_fn=None +): + """Returns the number of automorphisms of the graph. + + This function simply calls C{count_isomorphisms_vf2} with the graph + itgraph. See C{count_isomorphisms_vf2} for an explanation of the + parameters. + + @return: the number of automorphisms of the graph + @see: Graph.count_isomorphisms_vf2 + """ + return graph.count_isomorphisms_vf2( + graph, + color1=color, + color2=color, + edge_color1=edge_color, + edge_color2=edge_color, + node_compat_fn=node_compat_fn, + edge_compat_fn=edge_compat_fn, + ) + + +def _get_automorphisms_vf2( + graph, color=None, edge_color=None, node_compat_fn=None, edge_compat_fn=None +): + """Returns all the automorphisms of the graph + + This function simply calls C{get_isomorphisms_vf2} with the graph + itgraph. See C{get_isomorphisms_vf2} for an explanation of the + parameters. + + @return: a list of lists, each item containing a possible mapping + of the graph vertices to itgraph according to the automorphism + @see: Graph.get_isomorphisms_vf2 + """ + return graph.get_isomorphisms_vf2( + graph, + color1=color, + color2=color, + edge_color1=edge_color, + edge_color2=edge_color, + node_compat_fn=node_compat_fn, + edge_compat_fn=edge_compat_fn, + ) + diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 3aaacdc49..7ef002f1b 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -437,7 +437,7 @@ def fit_into(self, bbox, keep_aspect_ratio=True): self.translate(*translations) -def _layout(self, layout=None, *args, **kwds): +def _layout(graph, layout=None, *args, **kwds): """Returns the layout of the graph according to a layout algorithm. Parameters and keyword arguments not specified here are passed to the @@ -524,16 +524,16 @@ def _layout(self, layout=None, *args, **kwds): elif layout[-2:] == "3d": kwds["dim"] = 3 layout = layout[:-2] - method = getattr(self.__class__, self._layout_mapping[layout]) + method = getattr(graph.__class__, graph._layout_mapping[layout]) if not hasattr(method, "__call__"): raise ValueError("layout method must be callable") - layout = method(self, *args, **kwds) + layout = method(graph, *args, **kwds) if not isinstance(layout, Layout): layout = Layout(layout) return layout -def _layout_auto(self, *args, **kwds): +def _layout_auto(graph, *args, **kwds): """Chooses and runs a suitable layout function based on simple topological properties of the graph. @@ -570,8 +570,8 @@ def _layout_auto(self, *args, **kwds): 3D layout. @return: a L{Layout} object. """ - if "layout" in self.attributes(): - layout = self["layout"] + if "layout" in graph.attributes(): + layout = graph["layout"] if isinstance(layout, Layout): # Layouts are used intact return layout @@ -582,27 +582,27 @@ def _layout_auto(self, *args, **kwds): # Callables are called return Layout(layout(*args, **kwds)) # Try Graph.layout() - return self.layout(layout, *args, **kwds) + return graph.layout(layout, *args, **kwds) dim = kwds.get("dim", 2) - vattrs = self.vertex_attributes() + vattrs = graph.vertex_attributes() if "x" in vattrs and "y" in vattrs: if dim == 3 and "z" in vattrs: - return Layout(list(zip(self.vs["x"], self.vs["y"], self.vs["z"]))) + return Layout(list(zip(graph.vs["x"], graph.vs["y"], graph.vs["z"]))) else: - return Layout(list(zip(self.vs["x"], self.vs["y"]))) + return Layout(list(zip(graph.vs["x"], graph.vs["y"]))) - if self.vcount() <= 100 and self.is_connected(): + if graph.vcount() <= 100 and graph.is_connected(): algo = "kk" - elif self.vcount() <= 1000: + elif graph.vcount() <= 1000: algo = "fr" else: algo = "drl" - return self.layout(algo, *args, **kwds) + return graph.layout(algo, *args, **kwds) def _layout_sugiyama( - self, + graph, layers=None, weights=None, hgap=1, @@ -665,12 +665,12 @@ def _layout_sugiyama( if not return_extended_graph: return Layout( GraphBase._layout_sugiyama( - self, layers, weights, hgap, vgap, maxiter, return_extended_graph + graph, layers, weights, hgap, vgap, maxiter, return_extended_graph ) ) layout, extd_graph, extd_to_orig_eids = GraphBase._layout_sugiyama( - self, layers, weights, hgap, vgap, maxiter, return_extended_graph + graph, layers, weights, hgap, vgap, maxiter, return_extended_graph ) extd_graph.es["_original_eid"] = extd_to_orig_eids return Layout(layout), extd_graph From a37947217405997aa7863a599dfce73b80cd0b1b Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 5 Oct 2021 09:13:58 +1100 Subject: [PATCH 0474/1681] More modules, code blocks in __init__.py, reordering --- src/igraph/__init__.py | 1354 ++++++++---------------------- src/igraph/adjacency.py | 175 ++++ src/igraph/basic/__init__.py | 27 + src/igraph/bipartite.py | 125 +++ src/igraph/clustering.py | 61 ++ src/igraph/community/__init__.py | 34 + src/igraph/components.py | 0 src/igraph/cut.py | 142 ++++ src/igraph/io/__init__.py | 25 + src/igraph/layout.py | 34 + src/igraph/seq/__init__.py | 104 +++ src/igraph/seq/edgeseq.py | 431 ---------- src/igraph/structural.py | 95 +++ 13 files changed, 1162 insertions(+), 1445 deletions(-) create mode 100644 src/igraph/adjacency.py create mode 100644 src/igraph/bipartite.py create mode 100644 src/igraph/components.py delete mode 100644 src/igraph/seq/edgeseq.py create mode 100644 src/igraph/structural.py diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index e7f65ec19..a8c9fc59e 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -75,12 +75,31 @@ set_status_handler, __igraph_version__, ) +from igraph.adjacency import ( + _get_adjacency, + _get_adjacency_sparse, + _get_adjlist, + _get_incidence, + _get_inclist, +) +from igraph.automorphisms import ( + _count_automorphisms_vf2, + _get_automorphisms_vf2, +) from igraph.basic import ( _add_edge, _add_edges, _add_vertex, _add_vertices, _delete_edges, + _clear, + _as_directed, + _as_undirected, +) +from igraph.bipartite import ( + _maximum_bipartite_matching, + _bipartite_projection, + _bipartite_projection_size, ) from igraph.community import ( _community_fastgreedy, @@ -95,6 +114,7 @@ _community_walktrap, _k_core, _community_leiden, + _modularity, ) from igraph.clustering import ( Clustering, @@ -106,12 +126,20 @@ CohesiveBlocks, compare_communities, split_join_distance, + _biconnected_components, + _cohesive_blocks, + _clusters, ) -from igraph.automorphisms import ( - _count_automorphisms_vf2, - _get_automorphisms_vf2, +from igraph.cut import ( + Cut, + Flow, + _all_st_cuts, + _all_st_mincuts, + _gomory_hu_tree, + _maxflow, + _mincut, + _st_mincut, ) -from igraph.cut import Cut, Flow from igraph.configuration import Configuration, init as init_configuration from igraph.drawing import ( BoundingBox, @@ -147,6 +175,7 @@ from igraph.drawing.utils import autocurve from igraph.datatypes import Matrix, DyadCensus, TriadCensus, UniqueIdGenerator from igraph.formula import construct_graph_from_formula +from igraph.io import _format_mapping from igraph.io.files import ( _construct_graph_from_graphmlz_file, _construct_graph_from_dimacs_file, @@ -201,6 +230,7 @@ _layout_sugiyama, _layout_method_wrapper, _3d_version_for, + _layout_mapping, ) from igraph.matching import Matching from igraph.operators import ( @@ -209,7 +239,7 @@ intersection, operator_method_registry as _operator_method_registry, ) -from igraph.seq import EdgeSeq, VertexSeq +from igraph.seq import EdgeSeq, VertexSeq, _add_proxy_methods from igraph.statistics import ( FittedPowerLaw, Histogram, @@ -220,6 +250,12 @@ quantile, power_law_fit, ) +from igraph.structural import ( + _indegree, + _outdegree, + _degree_distribution, + _pagerank, +) from igraph.summary import GraphSummary, summary from igraph.utils import ( dbl_epsilon, @@ -427,307 +463,238 @@ def __init__(self, *args, **kwds): key = str(key) self.es[key] = value - def as_directed(self, *args, **kwds): - """Returns a directed copy of this graph. Arguments are passed on - to L{to_directed()} that is invoked on the copy. - """ - copy = self.copy() - copy.to_directed(*args, **kwds) - return copy - - def as_undirected(self, *args, **kwds): - """Returns an undirected copy of this graph. Arguments are passed on - to L{to_undirected()} that is invoked on the copy. - """ - copy = self.copy() - copy.to_undirected(*args, **kwds) - return copy + ############################################# + # Auxiliary I/O functions + # Graph libraries + from_networkx = classmethod(_construct_graph_from_networkx) + to_networkx = _export_graph_to_networkx - def clear(self): - """Clears the graph, deleting all vertices, edges, and attributes. + from_graph_tool = classmethod(_construct_graph_from_graph_tool) + to_graph_tool = _export_graph_to_graph_tool - @see: L{delete_vertices} and L{delete_edges}. - """ - self.delete_vertices() - for attr in self.attributes(): - del self[attr] + # Files + Read_DIMACS = classmethod(_construct_graph_from_dimacs_file) + write_dimacs = _write_graph_to_dimacs_file - # Basic operations - add_edge = _add_edge + Read_GraphMLz = classmethod(_construct_graph_from_graphmlz_file) + write_graphmlz = _write_graph_to_graphmlz_file - add_edges = _add_edges + Read_Pickle = classmethod(_construct_graph_from_pickle_file) + write_pickle = _write_graph_to_pickle_file - add_vertex = _add_vertex + Read_Picklez = classmethod(_construct_graph_from_picklez_file) + write_picklez = _write_graph_to_picklez_file - add_vertices = _add_vertices + Read_Adjacency = classmethod(_construct_graph_from_adjacency_file) + write_adjacency = _write_graph_to_adjacency_file - delete_edges = _delete_edges + Read = classmethod(_construct_graph_from_file) + Load = Read + write = _write_graph_to_file + save = write - # Structural properties - def indegree(self, *args, **kwds): - """Returns the in-degrees in a list. + # Various objects + # list of dict representation of graphs + DictList = classmethod(_construct_graph_from_dict_list) + to_dict_list = _export_graph_to_dict_list - See L{degree} for possible arguments. - """ - kwds["mode"] = IN - return self.degree(*args, **kwds) + # tuple-like representation of graphs + TupleList = classmethod(_construct_graph_from_tuple_list) + to_tuple_list = _export_graph_to_tuple_list - def outdegree(self, *args, **kwds): - """Returns the out-degrees in a list. + # dict of sequence representation of graphs + ListDict = classmethod(_construct_graph_from_list_dict) + to_list_dict = _export_graph_to_list_dict - See L{degree} for possible arguments. - """ - kwds["mode"] = OUT - return self.degree(*args, **kwds) + # dict of dicts representation of graphs + DictDict = classmethod(_construct_graph_from_dict_dict) + to_dict_dict = _export_graph_to_dict_dict - def all_st_cuts(self, source, target): - """\ - Returns all the cuts between the source and target vertices in a - directed graph. + # adjacency matrix + Adjacency = classmethod(_construct_graph_from_adjacency) + Weighted_Adjacency = classmethod(_construct_graph_from_weighted_adjacency) - This function lists all edge-cuts between a source and a target vertex. - Every cut is listed exactly once. + # pandas dataframe(s) + DataFrame = classmethod(_construct_graph_from_dataframe) + get_vertex_dataframe = _export_vertex_dataframe + get_edge_dataframe = _export_edge_dataframe - @param source: the source vertex ID - @param target: the target vertex ID - @return: a list of L{Cut} objects. + # Bipartite graphs + Bipartite = classmethod(_construct_bipartite_graph) + Incidence = classmethod(_construct_incidence_bipartite_graph) + Full_Bipartite = classmethod(_construct_full_bipartite_graph) + Random_Bipartite = classmethod(_construct_random_bipartite_graph) - @newfield ref: Reference - @ref: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in - graphs. Algorithmica 15, 351--372, 1996. - """ - return [ - Cut(self, cut=cut, partition=part) - for cut, part in zip(*GraphBase.all_st_cuts(self, source, target)) - ] + # Other constructors + GRG = classmethod(_construct_random_geometric_graph) - def all_st_mincuts(self, source, target, capacity=None): - """\ - Returns all the mincuts between the source and target vertices in a - directed graph. + # Graph formulae + Formula = classmethod(construct_graph_from_formula) - This function lists all minimum edge-cuts between a source and a target - vertex. + ############################################# + # Summary/string representation + def __str__(self): + """Returns a string representation of the graph. - @param source: the source vertex ID - @param target: the target vertex ID - @param capacity: the edge capacities (weights). If C{None}, all - edges have equal weight. May also be an attribute name. - @return: a list of L{Cut} objects. + Behind the scenes, this method constructs a L{GraphSummary} + instance and invokes its C{__str__} method with a verbosity of 1 + and attribute printing turned off. - @newfield ref: Reference - @ref: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in - graphs. Algorithmica 15, 351--372, 1996. - """ - value, cuts, parts = GraphBase.all_st_mincuts(self, source, target, capacity) - return [ - Cut(self, value, cut=cut, partition=part) for cut, part in zip(cuts, parts) - ] - - def biconnected_components(self, return_articulation_points=False): - """\ - Calculates the biconnected components of the graph. - - @param return_articulation_points: whether to return the articulation - points as well - @return: a L{VertexCover} object describing the biconnected components, - and optionally the list of articulation points as well + See the documentation of L{GraphSummary} for more details about the + output. """ - if return_articulation_points: - trees, aps = GraphBase.biconnected_components(self, True) - else: - trees = GraphBase.biconnected_components(self, False) - - clusters = [] - if trees: - edgelist = self.get_edgelist() - for tree in trees: - cluster = set() - for edge_id in tree: - cluster.update(edgelist[edge_id]) - clusters.append(sorted(cluster)) + params = dict( + verbosity=1, + width=78, + print_graph_attributes=False, + print_vertex_attributes=False, + print_edge_attributes=False, + ) + return self.summary(**params) - clustering = VertexCover(self, clusters) + def summary(self, verbosity=0, width=None, *args, **kwds): + """Returns the summary of the graph. - if return_articulation_points: - return clustering, aps - else: - return clustering + The output of this method is similar to the output of the + C{__str__} method. If I{verbosity} is zero, only the header line + is returned (see C{__str__} for more details), otherwise the + header line and the edge list is printed. - blocks = biconnected_components + Behind the scenes, this method constructs a L{GraphSummary} + object and invokes its C{__str__} method. - def cohesive_blocks(self): - """Calculates the cohesive block structure of the graph. - - Cohesive blocking is a method of determining hierarchical subsets of graph - vertices based on their structural cohesion (i.e. vertex connectivity). - For a given graph G, a subset of its vertices S is said to be maximally - k-cohesive if there is no superset of S with vertex connectivity greater - than or equal to k. Cohesive blocking is a process through which, given a - k-cohesive set of vertices, maximally l-cohesive subsets are recursively - identified with l > k. Thus a hierarchy of vertex subsets is obtained in - the end, with the entire graph G at its root. - - @return: an instance of L{CohesiveBlocks}. See the documentation of - L{CohesiveBlocks} for more information. - @see: L{CohesiveBlocks} + @param verbosity: if zero, only the header line is returned + (see C{__str__} for more details), otherwise the header line + and the full edge list is printed. + @param width: the number of characters to use in one line. + If C{None}, no limit will be enforced on the line lengths. + @return: the summary of the graph. """ - return CohesiveBlocks(self, *GraphBase.cohesive_blocks(self)) - - def clusters(self, mode="strong"): - """Calculates the (strong or weak) clusters (connected components) for - a given graph. - - @param mode: must be either C{"strong"} or C{"weak"}, depending on the - clusters being sought. Optional, defaults to C{"strong"}. - @return: a L{VertexClustering} object""" - return VertexClustering(self, GraphBase.clusters(self, mode)) + return str(GraphSummary(self, verbosity, width, *args, **kwds)) - components = clusters + ############################################# + # Commonly used attributes + def is_named(self): + """Returns whether the graph is named. - def degree_distribution(self, bin_width=1, *args, **kwds): - """Calculates the degree distribution of the graph. + A graph is named if and only if it has a C{"name"} vertex attribute. + """ + return "name" in self.vertex_attributes() - Unknown keyword arguments are directly passed to L{degree()}. + def is_weighted(self): + """Returns whether the graph is weighted. - @param bin_width: the bin width of the histogram - @return: a histogram representing the degree distribution of the - graph. + A graph is weighted if and only if it has a C{"weight"} edge attribute. """ - result = Histogram(bin_width, self.degree(*args, **kwds)) - return result - - def dyad_census(self, *args, **kwds): - """Calculates the dyad census of the graph. + return "weight" in self.edge_attributes() - Dyad census means classifying each pair of vertices of a directed - graph into three categories: mutual (there is an edge from I{a} to - I{b} and also from I{b} to I{a}), asymmetric (there is an edge - from I{a} to I{b} or from I{b} to I{a} but not the other way round) - and null (there is no connection between I{a} and I{b}). + ############################################# + # Vertex and edge sequence + @property + def vs(self): + """The vertex sequence of the graph""" + return VertexSeq(self) - @return: a L{DyadCensus} object. - @newfield ref: Reference - @ref: Holland, P.W. and Leinhardt, S. (1970). A Method for Detecting - Structure in Sociometric Data. American Journal of Sociology, 70, - 492-513. - """ - return DyadCensus(GraphBase.dyad_census(self, *args, **kwds)) + @property + def es(self): + """The edge sequence of the graph""" + return EdgeSeq(self) - def get_adjacency( - self, type=GET_ADJACENCY_BOTH, attribute=None, default=0, eids=False - ): - """Returns the adjacency matrix of a graph. - - @param type: either C{GET_ADJACENCY_LOWER} (uses the lower - triangle of the matrix) or C{GET_ADJACENCY_UPPER} - (uses the upper triangle) or C{GET_ADJACENCY_BOTH} - (uses both parts). Ignored for directed graphs. - @param attribute: if C{None}, returns the ordinary adjacency - matrix. When the name of a valid edge attribute is given - here, the matrix returned will contain the default value - at the places where there is no edge or the value of the - given attribute where there is an edge. Multiple edges are - not supported, the value written in the matrix in this case - will be unpredictable. This parameter is ignored if - I{eids} is C{True} - @param default: the default value written to the cells in the - case of adjacency matrices with attributes. - @param eids: specifies whether the edge IDs should be returned - in the adjacency matrix. Since zero is a valid edge ID, the - cells in the matrix that correspond to unconnected vertex - pairs will contain -1 instead of 0 if I{eids} is C{True}. - If I{eids} is C{False}, the number of edges will be returned - in the matrix for each vertex pair. - @return: the adjacency matrix as a L{Matrix}. - """ - if ( - type != GET_ADJACENCY_LOWER - and type != GET_ADJACENCY_UPPER - and type != GET_ADJACENCY_BOTH - ): - # Maybe it was called with the first argument as the attribute name - type, attribute = attribute, type - if type is None: - type = GET_ADJACENCY_BOTH - - if eids: - result = Matrix(GraphBase.get_adjacency(self, type, eids)) - result -= 1 - return result - - if attribute is None: - return Matrix(GraphBase.get_adjacency(self, type)) - - if attribute not in self.es.attribute_names(): - raise ValueError("Attribute does not exist") - - data = [[default] * self.vcount() for _ in range(self.vcount())] - - if self.is_directed(): - for edge in self.es: - data[edge.source][edge.target] = edge[attribute] - return Matrix(data) - - if type == GET_ADJACENCY_BOTH: - for edge in self.es: - source, target = edge.tuple - data[source][target] = edge[attribute] - data[target][source] = edge[attribute] - elif type == GET_ADJACENCY_UPPER: - for edge in self.es: - data[min(edge.tuple)][max(edge.tuple)] = edge[attribute] - else: - for edge in self.es: - data[max(edge.tuple)][min(edge.tuple)] = edge[attribute] + ############################################# + # Basic operations + add_edge = _add_edge + add_edges = _add_edges + add_vertex = _add_vertex + add_vertices = _add_vertices + delete_edges = _delete_edges + clear = _clear + as_directed = _as_directed + as_undirected = _as_undirected - return Matrix(data) + ################### + # Graph operators + __iadd__ = _operator_method_registry['__iadd__'] + __add__ = _operator_method_registry['__add__'] + __and__ = _operator_method_registry['__and__'] + __isub__ = _operator_method_registry['__isub__'] + __sub__ = _operator_method_registry['__sub__'] + __mul__ = _operator_method_registry['__mul__'] + __or__ = _operator_method_registry['__or__'] + disjoint_union = _operator_method_registry['disjoint_union'] + union = _operator_method_registry['union'] + intersection = _operator_method_registry['intersection'] - def get_adjacency_sparse(self, attribute=None): - """Returns the adjacency matrix of a graph as a SciPy CSR matrix. + ############################################# + # Adjacency/incidence + get_adjacency = _get_adjacency + get_adjacency_sparse = _get_adjacency_sparse + get_adjlist = _get_adjlist + get_incidence = _get_incidence + get_inclist = _get_inclist - @param attribute: if C{None}, returns the ordinary adjacency - matrix. When the name of a valid edge attribute is given - here, the matrix returned will contain the default value - at the places where there is no edge or the value of the - given attribute where there is an edge. - @return: the adjacency matrix as a C{scipy.sparse.csr_matrix}. - """ - try: - from scipy import sparse - except ImportError: - raise ImportError("You should install scipy in order to use this function") + ############################################# + # Structural properties + indegree = _indegree + outdegree = _outdegree + degree_distribution = _degree_distribution + pagerank = _pagerank - edges = self.get_edgelist() - if attribute is None: - weights = [1] * len(edges) - else: - if attribute not in self.es.attribute_names(): - raise ValueError("Attribute does not exist") + ############################################# + # Flow + all_st_cuts = _all_st_cuts + all_st_mincuts = _all_st_mincuts + gomory_hu_tree = _gomory_hu_tree + maxflow = _maxflow + mincut = _mincut + st_mincut = _st_mincut - weights = self.es[attribute] + ############################################# + # Connected components + biconnected_components = _biconnected_components + cohesive_blocks = _cohesive_blocks + clusters = _clusters + blocks = biconnected_components + components = clusters - N = self.vcount() - mtx = sparse.csr_matrix((weights, list(zip(*edges))), shape=(N, N)) + ############################################# + # Community detection/clustering + community_fastgreedy = _community_fastgreedy + community_infomap = _community_infomap + community_leading_eigenvector_naive = _community_leading_eigenvector_naive + community_leading_eigenvector = _community_leading_eigenvector + community_label_propagation = _community_label_propagation + community_multilevel = _community_multilevel + community_optimal_modularity = _community_optimal_modularity + community_edge_betweenness = _community_edge_betweenness + community_spinglass = _community_spinglass + community_walktrap = _community_walktrap + k_core = _k_core + community_leiden = _community_leiden + modularity = _modularity - if not self.is_directed(): - mtx = mtx + sparse.triu(mtx, 1).T + sparse.tril(mtx, -1).T - return mtx + ############################################# + # Layout + layout = _layout + layout_auto = _layout_auto + layout_sugiyama = _layout_sugiyama - def get_adjlist(self, mode="out"): - """Returns the adjacency list representation of the graph. + ############################################# + # Plotting + __plot__ = _graph_plot - The adjacency list representation is a list of lists. Each item of the - outer list belongs to a single vertex of the graph. The inner list - contains the neighbors of the given vertex. + ############################################# + # Bipartite + maximum_bipartite_matching = _maximum_bipartite_matching + bipartite_projection = _bipartite_projection + bipartite_projection_size = _bipartite_projection_size - @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all"\"}, both - the predecessors and the successors will be returned. Ignored - for undirected graphs. - """ - return [self.neighbors(idx, mode) for idx in range(self.vcount())] + ############################################# + # Automorphisms + count_automorphisms_vf2 = _count_automorphisms_vf2 + get_automorphisms_vf2 = _get_automorphisms_vf2 + ########################### + # Paths/traversals def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): """Calculates all the simple paths from a given node to some other nodes (or all of them) in a graph. @@ -762,159 +729,6 @@ def get_all_simple_paths(self, v, to=None, cutoff=-1, mode="out"): prev = index + 1 return result - def get_inclist(self, mode="out"): - """Returns the incidence list representation of the graph. - - The incidence list representation is a list of lists. Each - item of the outer list belongs to a single vertex of the graph. - The inner list contains the IDs of the incident edges of the - given vertex. - - @param mode: if C{\"out\"}, returns the successors of the vertex. If - C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both - the predecessors and the successors will be returned. Ignored - for undirected graphs. - """ - return [self.incident(idx, mode) for idx in range(self.vcount())] - - def gomory_hu_tree(self, capacity=None, flow="flow"): - """Calculates the Gomory-Hu tree of an undirected graph with optional - edge capacities. - - The Gomory-Hu tree is a concise representation of the value of all the - maximum flows (or minimum cuts) in a graph. The vertices of the tree - correspond exactly to the vertices of the original graph in the same order. - Edges of the Gomory-Hu tree are annotated by flow values. The value of - the maximum flow (or minimum cut) between an arbitrary (u,v) vertex - pair in the original graph is then given by the minimum flow value (i.e. - edge annotation) along the shortest path between u and v in the - Gomory-Hu tree. - - @param capacity: the edge capacities (weights). If C{None}, all - edges have equal weight. May also be an attribute name. - @param flow: the name of the edge attribute in the returned graph - in which the flow values will be stored. - @return: the Gomory-Hu tree as a L{Graph} object. - """ - graph, flow_values = GraphBase.gomory_hu_tree(self, capacity) - graph.es[flow] = flow_values - return graph - - def is_named(self): - """Returns whether the graph is named. - - A graph is named if and only if it has a C{"name"} vertex attribute. - """ - return "name" in self.vertex_attributes() - - def is_weighted(self): - """Returns whether the graph is weighted. - - A graph is weighted if and only if it has a C{"weight"} edge attribute. - """ - return "weight" in self.edge_attributes() - - def maxflow(self, source, target, capacity=None): - """Returns a maximum flow between the given source and target vertices - in a graph. - - A maximum flow from I{source} to I{target} is an assignment of - non-negative real numbers to the edges of the graph, satisfying - two properties: - - 1. For each edge, the flow (i.e. the assigned number) is not - more than the capacity of the edge (see the I{capacity} - argument) - - 2. For every vertex except the source and the target, the - incoming flow is the same as the outgoing flow. - - The value of the flow is the incoming flow of the target or the - outgoing flow of the source (which are equal). The maximum flow - is the maximum possible such value. - - @param capacity: the edge capacities (weights). If C{None}, all - edges have equal weight. May also be an attribute name. - @return: a L{Flow} object describing the maximum flow - """ - return Flow(self, *GraphBase.maxflow(self, source, target, capacity)) - - def mincut(self, source=None, target=None, capacity=None): - """Calculates the minimum cut between the given source and target vertices - or within the whole graph. - - The minimum cut is the minimum set of edges that needs to be removed to - separate the source and the target (if they are given) or to disconnect the - graph (if neither the source nor the target are given). The minimum is - calculated using the weights (capacities) of the edges, so the cut with - the minimum total capacity is calculated. - - For undirected graphs and no source and target, the method uses the - Stoer-Wagner algorithm. For a given source and target, the method uses the - push-relabel algorithm; see the references below. - - @param source: the source vertex ID. If C{None}, the target must also be - C{None} and the calculation will be done for the entire graph (i.e. - all possible vertex pairs). - @param target: the target vertex ID. If C{None}, the source must also be - C{None} and the calculation will be done for the entire graph (i.e. - all possible vertex pairs). - @param capacity: the edge capacities (weights). If C{None}, all - edges have equal weight. May also be an attribute name. - @return: a L{Cut} object describing the minimum cut - """ - return Cut(self, *GraphBase.mincut(self, source, target, capacity)) - - def st_mincut(self, source, target, capacity=None): - """Calculates the minimum cut between the source and target vertices in a - graph. - - @param source: the source vertex ID - @param target: the target vertex ID - @param capacity: the capacity of the edges. It must be a list or a valid - attribute name or C{None}. In the latter case, every edge will have the - same capacity. - @return: the value of the minimum cut, the IDs of vertices in the - first and second partition, and the IDs of edges in the cut, - packed in a 4-tuple - """ - return Cut(self, *GraphBase.st_mincut(self, source, target, capacity)) - - def modularity(self, membership, weights=None): - """Calculates the modularity score of the graph with respect to a given - clustering. - - The modularity of a graph w.r.t. some division measures how good the - division is, or how separated are the different vertex types from each - other. It's defined as M{Q=1/(2m)*sum(Aij-ki*kj/(2m)delta(ci,cj),i,j)}. - M{m} is the number of edges, M{Aij} is the element of the M{A} - adjacency matrix in row M{i} and column M{j}, M{ki} is the degree of - node M{i}, M{kj} is the degree of node M{j}, and M{Ci} and C{cj} are - the types of the two vertices (M{i} and M{j}). M{delta(x,y)} is one iff - M{x=y}, 0 otherwise. - - If edge weights are given, the definition of modularity is modified as - follows: M{Aij} becomes the weight of the corresponding edge, M{ki} - is the total weight of edges adjacent to vertex M{i}, M{kj} is the - total weight of edges adjacent to vertex M{j} and M{m} is the total - edge weight in the graph. - - @param membership: a membership list or a L{VertexClustering} object - @param weights: optional edge weights or C{None} if all edges are - weighed equally. Attribute names are also allowed. - @return: the modularity score - - @newfield ref: Reference - @ref: MEJ Newman and M Girvan: Finding and evaluating community - structure in networks. Phys Rev E 69 026113, 2004. - """ - if isinstance(membership, VertexClustering): - if membership.graph != self: - raise ValueError("clustering object belongs to another graph") - return GraphBase.modularity(self, membership.membership, weights) - else: - return GraphBase.modularity(self, membership, weights) - def path_length_hist(self, directed=True): """Returns the path length histogram of the graph @@ -933,69 +747,55 @@ def path_length_hist(self, directed=True): hist.unconnected = int(unconn) return hist - def pagerank( - self, - vertices=None, - directed=True, - damping=0.85, - weights=None, - arpack_options=None, - implementation="prpack", - niter=1000, - eps=0.001, - ): - """Calculates the PageRank values of a graph. - - @param vertices: the indices of the vertices being queried. - C{None} means all of the vertices. - @param directed: whether to consider directed paths. - @param damping: the damping factor. M{1-damping} is the PageRank value - for nodes with no incoming links. It is also the probability of - resetting the random walk to a uniform distribution in each step. - @param weights: edge weights to be used. Can be a sequence or iterable - or even an edge attribute name. - @param arpack_options: an L{ARPACKOptions} object used to fine-tune - the ARPACK eigenvector calculation. If omitted, the module-level - variable called C{arpack_options} is used. This argument is - ignored if not the ARPACK implementation is used, see the - I{implementation} argument. - @param implementation: which implementation to use to solve the - PageRank eigenproblem. Possible values are: - - C{"prpack"}: use the PRPACK library. This is a new - implementation in igraph 0.7 - - C{"arpack"}: use the ARPACK library. This implementation - was used from version 0.5, until version 0.7. - - C{"power"}: use a simple power method. This is the - implementation that was used before igraph version 0.5. - @param niter: The number of iterations to use in the power method - implementation. It is ignored in the other implementations - @param eps: The power method implementation will consider the - calculation as complete if the difference of PageRank values between - iterations change less than this value for every node. It is - ignored by the other implementations. - @return: a list with the Google PageRank values of the specified - vertices.""" - if arpack_options is None: - arpack_options = default_arpack_options - return self.personalized_pagerank( - vertices, - directed, - damping, - None, - None, - weights, - arpack_options, - implementation, - niter, - eps, - ) - - def spanning_tree(self, weights=None, return_tree=True): - """Calculates a minimum spanning tree for a graph. + # DFS (C version will come soon) + def dfs(self, vid, mode=OUT): + """Conducts a depth first search (DFS) on the graph. - @param weights: a vector containing weights for every edge in - the graph. C{None} means that the graph is unweighted. - @param return_tree: whether to return the minimum spanning tree (when + @param vid: the root vertex ID + @param mode: either C{\"in\"} or C{\"out\"} or C{\"all\"}, ignored + for undirected graphs. + @return: a tuple with the following items: + - The vertex IDs visited (in order) + - The parent of every vertex in the DFS + """ + nv = self.vcount() + added = [False for v in range(nv)] + stack = [] + + # prepare output + vids = [] + parents = [] + + # ok start from vid + stack.append((vid, self.neighbors(vid, mode=mode))) + vids.append(vid) + parents.append(vid) + added[vid] = True + + # go down the rabbit hole + while stack: + vid, neighbors = stack[-1] + if neighbors: + # Get next neighbor to visit + neighbor = neighbors.pop() + if not added[neighbor]: + # Add hanging subtree neighbor + stack.append((neighbor, self.neighbors(neighbor, mode=mode))) + vids.append(neighbor) + parents.append(vid) + added[neighbor] = True + else: + # No neighbor found, end of subtree + stack.pop() + + return (vids, parents) + + def spanning_tree(self, weights=None, return_tree=True): + """Calculates a minimum spanning tree for a graph. + + @param weights: a vector containing weights for every edge in + the graph. C{None} means that the graph is unweighted. + @param return_tree: whether to return the minimum spanning tree (when C{return_tree} is C{True}) or to return the IDs of the edges in the minimum spanning tree instead (when C{return_tree} is C{False}). The default is C{True} for historical reasons as this argument was @@ -1013,6 +813,39 @@ def spanning_tree(self, weights=None, return_tree=True): return self.subgraph_edges(result, delete_vertices=False) return result + ########################### + # Dyad/triad census + def dyad_census(self, *args, **kwds): + """Calculates the dyad census of the graph. + + Dyad census means classifying each pair of vertices of a directed + graph into three categories: mutual (there is an edge from I{a} to + I{b} and also from I{b} to I{a}), asymmetric (there is an edge + from I{a} to I{b} or from I{b} to I{a} but not the other way round) + and null (there is no connection between I{a} and I{b}). + + @return: a L{DyadCensus} object. + @newfield ref: Reference + @ref: Holland, P.W. and Leinhardt, S. (1970). A Method for Detecting + Structure in Sociometric Data. American Journal of Sociology, 70, + 492-513. + """ + return DyadCensus(GraphBase.dyad_census(self, *args, **kwds)) + + def triad_census(self, *args, **kwds): + """Calculates the triad census of the graph. + + @return: a L{TriadCensus} object. + @newfield ref: Reference + @ref: Davis, J.A. and Leinhardt, S. (1972). The Structure of + Positive Interpersonal Relations in Small Groups. In: + J. Berger (Ed.), Sociological Theories in Progress, Volume 2, + 218-251. Boston: Houghton Mifflin. + """ + return TriadCensus(GraphBase.triad_census(self, *args, **kwds)) + + ########################### + # Other functions def transitivity_avglocal_undirected(self, mode="nan", weights=None): """Calculates the average of the vertex transitivities of the graph. @@ -1051,344 +884,13 @@ def transitivity_avglocal_undirected(self, mode="nan", weights=None): xs = self.transitivity_local_undirected(mode=mode, weights=weights) return sum(xs) / float(len(xs)) - def triad_census(self, *args, **kwds): - """Calculates the triad census of the graph. - - @return: a L{TriadCensus} object. - @newfield ref: Reference - @ref: Davis, J.A. and Leinhardt, S. (1972). The Structure of - Positive Interpersonal Relations in Small Groups. In: - J. Berger (Ed.), Sociological Theories in Progress, Volume 2, - 218-251. Boston: Houghton Mifflin. - """ - return TriadCensus(GraphBase.triad_census(self, *args, **kwds)) - - # Automorphisms - count_automorphisms_vf2 = _count_automorphisms_vf2 - - get_automorphisms_vf2 = _get_automorphisms_vf2 - - # Various clustering algorithms -- mostly wrappers around GraphBase - community_fastgreedy = _community_fastgreedy - - community_infomap = _community_infomap - - community_leading_eigenvector_naive = _community_leading_eigenvector_naive - - community_leading_eigenvector = _community_leading_eigenvector - - community_label_propagation = _community_label_propagation - - community_multilevel = _community_multilevel - - community_optimal_modularity = _community_optimal_modularity - - community_edge_betweenness = _community_edge_betweenness - - community_spinglass = _community_spinglass - - community_walktrap = _community_walktrap - - k_core = _k_core - - community_leiden = _community_leiden - - layout = _layout - - layout_auto = _layout_auto - - layout_sugiyama = _layout_sugiyama - - def maximum_bipartite_matching(self, types="type", weights=None, eps=None): - """Finds a maximum matching in a bipartite graph. - - A maximum matching is a set of edges such that each vertex is incident on - at most one matched edge and the number (or weight) of such edges in the - set is as large as possible. - - @param types: vertex types in a list or the name of a vertex attribute - holding vertex types. Types should be denoted by zeros and ones (or - C{False} and C{True}) for the two sides of the bipartite graph. - If omitted, it defaults to C{type}, which is the default vertex type - attribute for bipartite graphs. - @param weights: edge weights to be used. Can be a sequence or iterable or - even an edge attribute name. - @param eps: a small real number used in equality tests in the weighted - bipartite matching algorithm. Two real numbers are considered equal in - the algorithm if their difference is smaller than this value. This - is required to avoid the accumulation of numerical errors. If you - pass C{None} here, igraph will try to determine an appropriate value - automatically. - @return: an instance of L{Matching}.""" - if eps is None: - eps = -1 - - matches = GraphBase._maximum_bipartite_matching(self, types, weights, eps) - return Matching(self, matches, types=types) - - ############################################# - # Auxiliary I/O functions - - # Graph libraries - from_networkx = classmethod(_construct_graph_from_networkx) - to_networkx = _export_graph_to_networkx - - from_graph_tool = classmethod(_construct_graph_from_graph_tool) - to_graph_tool = _export_graph_to_graph_tool - - # Files - Read_DIMACS = classmethod(_construct_graph_from_dimacs_file) - write_dimacs = _write_graph_to_dimacs_file - - Read_GraphMLz = classmethod(_construct_graph_from_graphmlz_file) - write_graphmlz = _write_graph_to_graphmlz_file - - Read_Pickle = classmethod(_construct_graph_from_pickle_file) - write_pickle = _write_graph_to_pickle_file - - Read_Picklez = classmethod(_construct_graph_from_picklez_file) - write_picklez = _write_graph_to_picklez_file - - Read_Adjacency = classmethod(_construct_graph_from_adjacency_file) - write_adjacency = _write_graph_to_adjacency_file - - Read = classmethod(_construct_graph_from_file) - Load = Read - write = _write_graph_to_file - save = write - - # Various objects - # list of dict representation of graphs - DictList = classmethod(_construct_graph_from_dict_list) - to_dict_list = _export_graph_to_dict_list - - # tuple-like representation of graphs - TupleList = classmethod(_construct_graph_from_tuple_list) - to_tuple_list = _export_graph_to_tuple_list - - # dict of sequence representation of graphs - ListDict = classmethod(_construct_graph_from_list_dict) - to_list_dict = _export_graph_to_list_dict - - # dict of dicts representation of graphs - DictDict = classmethod(_construct_graph_from_dict_dict) - to_dict_dict = _export_graph_to_dict_dict - - # adjacency matrix - Adjacency = classmethod(_construct_graph_from_adjacency) - - Weighted_Adjacency = classmethod(_construct_graph_from_weighted_adjacency) - - # pandas dataframe(s) - DataFrame = classmethod(_construct_graph_from_dataframe) - - get_vertex_dataframe = _export_vertex_dataframe - - get_edge_dataframe = _export_edge_dataframe - - # Bipartite graphs - Bipartite = classmethod(_construct_bipartite_graph) - - Incidence = classmethod(_construct_incidence_bipartite_graph) - - Full_Bipartite = classmethod(_construct_full_bipartite_graph) - - Random_Bipartite = classmethod(_construct_random_bipartite_graph) - - # Other constructors - GRG = classmethod(_construct_random_geometric_graph) - - # Graph formulae - Formula = classmethod(construct_graph_from_formula) - - ########################### - # Vertex and edge sequence - - @property - def vs(self): - """The vertex sequence of the graph""" - return VertexSeq(self) - - @property - def es(self): - """The edge sequence of the graph""" - return EdgeSeq(self) - - def bipartite_projection( - self, types="type", multiplicity=True, probe1=-1, which="both" - ): - """Projects a bipartite graph into two one-mode graphs. Edge directions - are ignored while projecting. - - Examples: - - >>> g = Graph.Full_Bipartite(10, 5) - >>> g1, g2 = g.bipartite_projection() - >>> g1.isomorphic(Graph.Full(10)) - True - >>> g2.isomorphic(Graph.Full(5)) - True - - @param types: an igraph vector containing the vertex types, or an - attribute name. Anything that evalulates to C{False} corresponds to - vertices of the first kind, everything else to the second kind. - @param multiplicity: if C{True}, then igraph keeps the multiplicity of - the edges in the projection in an edge attribute called C{"weight"}. - E.g., if there is an A-C-B and an A-D-B triplet in the bipartite - graph and there is no other X (apart from X=B and X=D) for which an - A-X-B triplet would exist in the bipartite graph, the multiplicity - of the A-B edge in the projection will be 2. - @param probe1: this argument can be used to specify the order of the - projections in the resulting list. If given and non-negative, then - it is considered as a vertex ID; the projection containing the - vertex will be the first one in the result. - @param which: this argument can be used to specify which of the two - projections should be returned if only one of them is needed. Passing - 0 here means that only the first projection is returned, while 1 means - that only the second projection is returned. (Note that we use 0 and 1 - because Python indexing is zero-based). C{False} is equivalent to 0 and - C{True} is equivalent to 1. Any other value means that both projections - will be returned in a tuple. - @return: a tuple containing the two projected one-mode graphs if C{which} - is not 1 or 2, or the projected one-mode graph specified by the - C{which} argument if its value is 0, 1, C{False} or C{True}. - """ - superclass_meth = super().bipartite_projection - - if which is False: - which = 0 - elif which is True: - which = 1 - if which != 0 and which != 1: - which = -1 - - if multiplicity: - if which == 0: - g1, w1 = superclass_meth(types, True, probe1, which) - g2, w2 = None, None - elif which == 1: - g1, w1 = None, None - g2, w2 = superclass_meth(types, True, probe1, which) - else: - g1, g2, w1, w2 = superclass_meth(types, True, probe1, which) - - if g1 is not None: - g1.es["weight"] = w1 - if g2 is not None: - g2.es["weight"] = w2 - return g1, g2 - else: - return g1 - else: - g2.es["weight"] = w2 - return g2 - else: - return superclass_meth(types, False, probe1, which) - - def bipartite_projection_size(self, types="type", *args, **kwds): - """Calculates the number of vertices and edges in the bipartite - projections of this graph according to the specified vertex types. - This is useful if you have a bipartite graph and you want to estimate - the amount of memory you would need to calculate the projections - themselves. - - @param types: an igraph vector containing the vertex types, or an - attribute name. Anything that evalulates to C{False} corresponds to - vertices of the first kind, everything else to the second kind. - @return: a 4-tuple containing the number of vertices and edges in the - first projection, followed by the number of vertices and edges in the - second projection. - """ - return super().bipartite_projection_size(types, *args, **kwds) - - def get_incidence(self, types="type", *args, **kwds): - """Returns the incidence matrix of a bipartite graph. The incidence matrix - is an M{n} times M{m} matrix, where M{n} and M{m} are the number of - vertices in the two vertex classes. - - @param types: an igraph vector containing the vertex types, or an - attribute name. Anything that evalulates to C{False} corresponds to - vertices of the first kind, everything else to the second kind. - @return: the incidence matrix and two lists in a triplet. The first - list defines the mapping between row indices of the matrix and the - original vertex IDs. The second list is the same for the column - indices. - """ - return super().get_incidence(types, *args, **kwds) - - ########################### - # DFS (C version will come soon) - def dfs(self, vid, mode=OUT): - """Conducts a depth first search (DFS) on the graph. - - @param vid: the root vertex ID - @param mode: either C{\"in\"} or C{\"out\"} or C{\"all\"}, ignored - for undirected graphs. - @return: a tuple with the following items: - - The vertex IDs visited (in order) - - The parent of every vertex in the DFS - """ - nv = self.vcount() - added = [False for v in range(nv)] - stack = [] - - # prepare output - vids = [] - parents = [] - - # ok start from vid - stack.append((vid, self.neighbors(vid, mode=mode))) - vids.append(vid) - parents.append(vid) - added[vid] = True - - # go down the rabbit hole - while stack: - vid, neighbors = stack[-1] - if neighbors: - # Get next neighbor to visit - neighbor = neighbors.pop() - if not added[neighbor]: - # Add hanging subtree neighbor - stack.append((neighbor, self.neighbors(neighbor, mode=mode))) - vids.append(neighbor) - parents.append(vid) - added[neighbor] = True - else: - # No neighbor found, end of subtree - stack.pop() - - return (vids, parents) - ########################### # ctypes support - @property def _as_parameter_(self): return self._raw_pointer() - ################### - # Custom operators - __iadd__ = _operator_method_registry['__iadd__'] - - __add__ = _operator_method_registry['__add__'] - - __and__ = _operator_method_registry['__and__'] - - __isub__ = _operator_method_registry['__isub__'] - - __sub__ = _operator_method_registry['__sub__'] - - __mul__ = _operator_method_registry['__mul__'] - - __or__ = _operator_method_registry['__or__'] - - disjoint_union = _operator_method_registry['disjoint_union'] - - union = _operator_method_registry['union'] - - intersection = _operator_method_registry['intersection'] - + # Other type functions def __bool__(self): """Returns True if the graph has at least one vertex, False otherwise.""" return self.vcount() > 0 @@ -1444,219 +946,24 @@ def __reduce__(self): __iter__ = None # needed for PyPy __hash__ = None # needed for PyPy - __plot__ = _graph_plot - - def __str__(self): - """Returns a string representation of the graph. - - Behind the scenes, this method constructs a L{GraphSummary} - instance and invokes its C{__str__} method with a verbosity of 1 - and attribute printing turned off. - - See the documentation of L{GraphSummary} for more details about the - output. - """ - params = dict( - verbosity=1, - width=78, - print_graph_attributes=False, - print_vertex_attributes=False, - print_edge_attributes=False, - ) - return self.summary(**params) - def summary(self, verbosity=0, width=None, *args, **kwds): - """Returns the summary of the graph. - - The output of this method is similar to the output of the - C{__str__} method. If I{verbosity} is zero, only the header line - is returned (see C{__str__} for more details), otherwise the - header line and the edge list is printed. - - Behind the scenes, this method constructs a L{GraphSummary} - object and invokes its C{__str__} method. - - @param verbosity: if zero, only the header line is returned - (see C{__str__} for more details), otherwise the header line - and the full edge list is printed. - @param width: the number of characters to use in one line. - If C{None}, no limit will be enforced on the line lengths. - @return: the summary of the graph. - """ - return str(GraphSummary(self, verbosity, width, *args, **kwds)) - - _format_mapping = { - "ncol": ("Read_Ncol", "write_ncol"), - "lgl": ("Read_Lgl", "write_lgl"), - "graphdb": ("Read_GraphDB", None), - "graphmlz": ("Read_GraphMLz", "write_graphmlz"), - "graphml": ("Read_GraphML", "write_graphml"), - "gml": ("Read_GML", "write_gml"), - "dot": (None, "write_dot"), - "graphviz": (None, "write_dot"), - "net": ("Read_Pajek", "write_pajek"), - "pajek": ("Read_Pajek", "write_pajek"), - "dimacs": ("Read_DIMACS", "write_dimacs"), - "adjacency": ("Read_Adjacency", "write_adjacency"), - "adj": ("Read_Adjacency", "write_adjacency"), - "edgelist": ("Read_Edgelist", "write_edgelist"), - "edge": ("Read_Edgelist", "write_edgelist"), - "edges": ("Read_Edgelist", "write_edgelist"), - "pickle": ("Read_Pickle", "write_pickle"), - "picklez": ("Read_Picklez", "write_picklez"), - "svg": (None, "write_svg"), - "gw": (None, "write_leda"), - "leda": (None, "write_leda"), - "lgr": (None, "write_leda"), - "dl": ("Read_DL", None), - } - - _layout_mapping = { - "auto": "layout_auto", - "automatic": "layout_auto", - "bipartite": "layout_bipartite", - "circle": "layout_circle", - "circular": "layout_circle", - "davidson_harel": "layout_davidson_harel", - "dh": "layout_davidson_harel", - "drl": "layout_drl", - "fr": "layout_fruchterman_reingold", - "fruchterman_reingold": "layout_fruchterman_reingold", - "graphopt": "layout_graphopt", - "grid": "layout_grid", - "kk": "layout_kamada_kawai", - "kamada_kawai": "layout_kamada_kawai", - "lgl": "layout_lgl", - "large": "layout_lgl", - "large_graph": "layout_lgl", - "mds": "layout_mds", - "random": "layout_random", - "rt": "layout_reingold_tilford", - "tree": "layout_reingold_tilford", - "reingold_tilford": "layout_reingold_tilford", - "rt_circular": "layout_reingold_tilford_circular", - "reingold_tilford_circular": "layout_reingold_tilford_circular", - "sphere": "layout_sphere", - "spherical": "layout_sphere", - "star": "layout_star", - "sugiyama": "layout_sugiyama", - } - - # After adjusting something here, don't forget to update the docstring - # of Graph.layout if necessary! +############################################################## +# I/O format mapping +Graph._format_mapping = _format_mapping ############################################################## # Additional methods of VertexSeq and EdgeSeq that call Graph methods +_add_proxy_methods() -def _graphmethod(func=None, name=None): - """Auxiliary decorator - - This decorator allows some methods of L{VertexSeq} and L{EdgeSeq} to - call their respective counterparts in L{Graph} to avoid code duplication. - - @param func: the function being decorated. This function will be - called on the results of the original L{Graph} method. - If C{None}, defaults to the identity function. - @param name: the name of the corresponding method in L{Graph}. If - C{None}, it defaults to the name of the decorated function. - @return: the decorated function - """ - if name is None: - name = func.__name__ - method = getattr(Graph, name) - - if hasattr(func, "__call__"): - - def decorated(*args, **kwds): - self = args[0].graph - return func(args[0], method(self, *args, **kwds)) - - else: - - def decorated(*args, **kwds): - self = args[0].graph - return method(self, *args, **kwds) - - decorated.__name__ = name - decorated.__doc__ = """Proxy method to L{Graph.%(name)s()} - -This method calls the C{%(name)s()} method of the L{Graph} class -restricted to this sequence, and returns the result. - -@see: Graph.%(name)s() for details. -""" % { - "name": name - } - - return decorated - - -def _add_proxy_methods(): - - # Proxy methods for VertexSeq and EdgeSeq that forward their arguments to - # the corresponding Graph method are constructed here. Proxy methods for - # Vertex and Edge are added in the C source code. Make sure that you update - # the C source whenever you add a proxy method here if that makes sense for - # an individual vertex or edge - decorated_methods = {} - decorated_methods[VertexSeq] = [ - "degree", - "betweenness", - "bibcoupling", - "closeness", - "cocitation", - "constraint", - "diversity", - "eccentricity", - "get_shortest_paths", - "maxdegree", - "pagerank", - "personalized_pagerank", - "shortest_paths", - "similarity_dice", - "similarity_jaccard", - "subgraph", - "indegree", - "outdegree", - "isoclass", - "delete_vertices", - "is_separator", - "is_minimal_separator", - ] - decorated_methods[EdgeSeq] = [ - "count_multiple", - "delete_edges", - "is_loop", - "is_multiple", - "is_mutual", - "subgraph_edges", - ] - - rename_methods = {} - rename_methods[VertexSeq] = {"delete_vertices": "delete"} - rename_methods[EdgeSeq] = {"delete_edges": "delete", "subgraph_edges": "subgraph"} - - for cls, methods in decorated_methods.items(): - for method in methods: - new_method_name = rename_methods[cls].get(method, method) - setattr(cls, new_method_name, _graphmethod(None, method)) - - setattr( - EdgeSeq, - "edge_betweenness", - _graphmethod( - lambda self, result: [result[i] for i in self.indices], "edge_betweenness" - ), - ) +############################################################## +# Layout mapping +Graph._layout_mapping = _layout_mapping -_add_proxy_methods() - ############################################################## # Making sure that layout methods always return a Layout - for name in dir(Graph): if not name.startswith("layout_"): continue @@ -1664,9 +971,9 @@ def _add_proxy_methods(): continue setattr(Graph, name, _layout_method_wrapper(getattr(Graph, name))) + ############################################################## # Adding aliases for the 3D versions of the layout methods - Graph.layout_fruchterman_reingold_3d = _3d_version_for( Graph.layout_fruchterman_reingold ) @@ -1675,9 +982,9 @@ def _add_proxy_methods(): Graph.layout_grid_3d = _3d_version_for(Graph.layout_grid) Graph.layout_sphere = _3d_version_for(Graph.layout_circle) -############################################################## - +############################################################## +# Auxiliary global functions def get_include(): """Returns the folder that contains the C API headers of the Python interface of igraph.""" @@ -1730,8 +1037,12 @@ def write(graph, filename, *args, **kwds): save = write +############################################################## +# Configuration singleton instance config = init_configuration() + +############################################################## # Remove modular methods from namespace del ( construct_graph_from_formula, @@ -1741,6 +1052,7 @@ def write(graph, filename, *args, **kwds): _construct_graph_from_picklez_file, _construct_graph_from_adjacency_file, _construct_graph_from_file, + _format_mapping, _construct_graph_from_dict_list, _construct_graph_from_tuple_list, _construct_graph_from_list_dict, @@ -1773,6 +1085,7 @@ def write(graph, filename, *args, **kwds): _community_walktrap, _k_core, _community_leiden, + _modularity, _graph_plot, _operator_method_registry, _add_edge, @@ -1780,11 +1093,24 @@ def write(graph, filename, *args, **kwds): _add_vertex, _add_vertices, _delete_edges, + _as_directed, + _as_undirected, _layout, _layout_auto, _layout_sugiyama, _layout_method_wrapper, _3d_version_for, + _layout_mapping, _count_automorphisms_vf2, _get_automorphisms_vf2, + _get_adjacency, + _get_adjacency_sparse, + _get_adjlist, + _maximum_bipartite_matching, + _bipartite_projection, + _bipartite_projection_size, + _biconnected_components, + _cohesive_blocks, + _clusters, + _add_proxy_methods, ) diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py new file mode 100644 index 000000000..65ae88606 --- /dev/null +++ b/src/igraph/adjacency.py @@ -0,0 +1,175 @@ +from igraph._igraph import ( + ADJ_DIRECTED, + ADJ_LOWER, + ADJ_MAX, + ADJ_MIN, + ADJ_PLUS, + ADJ_UNDIRECTED, + ADJ_UPPER, + GET_ADJACENCY_BOTH, + GET_ADJACENCY_LOWER, + GET_ADJACENCY_UPPER, + GraphBase, +) +from igraph.datatypes import Matrix + + +__all__ = ( + '_get_adjacency', + '_get_adjacency_sparse', + '_get_adjlist', + '_get_incidence', + '_get_inclist', +) + + +def _get_adjacency( + self, type=GET_ADJACENCY_BOTH, attribute=None, default=0, eids=False +): + """Returns the adjacency matrix of a graph. + + @param type: either C{GET_ADJACENCY_LOWER} (uses the lower + triangle of the matrix) or C{GET_ADJACENCY_UPPER} + (uses the upper triangle) or C{GET_ADJACENCY_BOTH} + (uses both parts). Ignored for directed graphs. + @param attribute: if C{None}, returns the ordinary adjacency + matrix. When the name of a valid edge attribute is given + here, the matrix returned will contain the default value + at the places where there is no edge or the value of the + given attribute where there is an edge. Multiple edges are + not supported, the value written in the matrix in this case + will be unpredictable. This parameter is ignored if + I{eids} is C{True} + @param default: the default value written to the cells in the + case of adjacency matrices with attributes. + @param eids: specifies whether the edge IDs should be returned + in the adjacency matrix. Since zero is a valid edge ID, the + cells in the matrix that correspond to unconnected vertex + pairs will contain -1 instead of 0 if I{eids} is C{True}. + If I{eids} is C{False}, the number of edges will be returned + in the matrix for each vertex pair. + @return: the adjacency matrix as a L{Matrix}. + """ + if ( + type != GET_ADJACENCY_LOWER + and type != GET_ADJACENCY_UPPER + and type != GET_ADJACENCY_BOTH + ): + # Maybe it was called with the first argument as the attribute name + type, attribute = attribute, type + if type is None: + type = GET_ADJACENCY_BOTH + + if eids: + result = Matrix(GraphBase.get_adjacency(self, type, eids)) + result -= 1 + return result + + if attribute is None: + return Matrix(GraphBase.get_adjacency(self, type)) + + if attribute not in self.es.attribute_names(): + raise ValueError("Attribute does not exist") + + data = [[default] * self.vcount() for _ in range(self.vcount())] + + if self.is_directed(): + for edge in self.es: + data[edge.source][edge.target] = edge[attribute] + return Matrix(data) + + if type == GET_ADJACENCY_BOTH: + for edge in self.es: + source, target = edge.tuple + data[source][target] = edge[attribute] + data[target][source] = edge[attribute] + elif type == GET_ADJACENCY_UPPER: + for edge in self.es: + data[min(edge.tuple)][max(edge.tuple)] = edge[attribute] + else: + for edge in self.es: + data[max(edge.tuple)][min(edge.tuple)] = edge[attribute] + + return Matrix(data) + + +def _get_adjacency_sparse(self, attribute=None): + """Returns the adjacency matrix of a graph as a SciPy CSR matrix. + + @param attribute: if C{None}, returns the ordinary adjacency + matrix. When the name of a valid edge attribute is given + here, the matrix returned will contain the default value + at the places where there is no edge or the value of the + given attribute where there is an edge. + @return: the adjacency matrix as a C{scipy.sparse.csr_matrix}. + """ + try: + from scipy import sparse + except ImportError: + raise ImportError("You should install scipy in order to use this function") + + edges = self.get_edgelist() + if attribute is None: + weights = [1] * len(edges) + else: + if attribute not in self.es.attribute_names(): + raise ValueError("Attribute does not exist") + + weights = self.es[attribute] + + N = self.vcount() + mtx = sparse.csr_matrix((weights, list(zip(*edges))), shape=(N, N)) + + if not self.is_directed(): + mtx = mtx + sparse.triu(mtx, 1).T + sparse.tril(mtx, -1).T + return mtx + + +def _get_adjlist(self, mode="out"): + """Returns the adjacency list representation of the graph. + + The adjacency list representation is a list of lists. Each item of the + outer list belongs to a single vertex of the graph. The inner list + contains the neighbors of the given vertex. + + @param mode: if C{\"out\"}, returns the successors of the vertex. If + C{\"in\"}, returns the predecessors of the vertex. If C{\"all"\"}, both + the predecessors and the successors will be returned. Ignored + for undirected graphs. + """ + return [self.neighbors(idx, mode) for idx in range(self.vcount())] + + +def _get_incidence(graph, types="type", *args, **kwds): + """Returns the incidence matrix of a bipartite graph. The incidence matrix + is an M{n} times M{m} matrix, where M{n} and M{m} are the number of + vertices in the two vertex classes. + + @param types: an igraph vector containing the vertex types, or an + attribute name. Anything that evalulates to C{False} corresponds to + vertices of the first kind, everything else to the second kind. + @return: the incidence matrix and two lists in a triplet. The first + list defines the mapping between row indices of the matrix and the + original vertex IDs. The second list is the same for the column + indices. + """ + # Deferred import to avoid cycles + from igraph import Graph + + return super(Graph, graph).get_incidence(types, *args, **kwds) + + +def _get_inclist(graph, mode="out"): + """Returns the incidence list representation of the graph. + + The incidence list representation is a list of lists. Each + item of the outer list belongs to a single vertex of the graph. + The inner list contains the IDs of the incident edges of the + given vertex. + + @param mode: if C{\"out\"}, returns the successors of the vertex. If + C{\"in\"}, returns the predecessors of the vertex. If C{\"all\"}, both + the predecessors and the successors will be returned. Ignored + for undirected graphs. + """ + return [graph.incident(idx, mode) for idx in range(graph.vcount())] diff --git a/src/igraph/basic/__init__.py b/src/igraph/basic/__init__.py index 7b7df7187..f02006a63 100644 --- a/src/igraph/basic/__init__.py +++ b/src/igraph/basic/__init__.py @@ -142,3 +142,30 @@ def _delete_edges(graph, *args, **kwds): else: edge_seq = args[0] return GraphBase.delete_edges(graph, edge_seq) + + +def _clear(graph): + """Clears the graph, deleting all vertices, edges, and attributes. + + @see: L{delete_vertices} and L{delete_edges}. + """ + graph.delete_vertices() + for attr in graph.attributes(): + del graph[attr] + + +def _as_directed(graph, *args, **kwds): + """Returns a directed copy of this graph. Arguments are passed on + to L{to_directed()} that is invoked on the copy. + """ + copy = graph.copy() + copy.to_directed(*args, **kwds) + return copy + +def _as_undirected(graph, *args, **kwds): + """Returns an undirected copy of this graph. Arguments are passed on + to L{to_undirected()} that is invoked on the copy. + """ + copy = graph.copy() + copy.to_undirected(*args, **kwds) + return copy diff --git a/src/igraph/bipartite.py b/src/igraph/bipartite.py new file mode 100644 index 000000000..881559401 --- /dev/null +++ b/src/igraph/bipartite.py @@ -0,0 +1,125 @@ +from igraph._igraph import GraphBase +from igraph.matching import Matching + + +def _maximum_bipartite_matching(graph, types="type", weights=None, eps=None): + """Finds a maximum matching in a bipartite graph. + + A maximum matching is a set of edges such that each vertex is incident on + at most one matched edge and the number (or weight) of such edges in the + set is as large as possible. + + @param types: vertex types in a list or the name of a vertex attribute + holding vertex types. Types should be denoted by zeros and ones (or + C{False} and C{True}) for the two sides of the bipartite graph. + If omitted, it defaults to C{type}, which is the default vertex type + attribute for bipartite graphs. + @param weights: edge weights to be used. Can be a sequence or iterable or + even an edge attribute name. + @param eps: a small real number used in equality tests in the weighted + bipartite matching algorithm. Two real numbers are considered equal in + the algorithm if their difference is smaller than this value. This + is required to avoid the accumulation of numerical errors. If you + pass C{None} here, igraph will try to determine an appropriate value + automatically. + @return: an instance of L{Matching}.""" + if eps is None: + eps = -1 + + matches = GraphBase._maximum_bipartite_matching(graph, types, weights, eps) + return Matching(graph, matches, types=types) + + +def _bipartite_projection( + graph, types="type", multiplicity=True, probe1=-1, which="both" +): + """Projects a bipartite graph into two one-mode graphs. Edge directions + are ignored while projecting. + + Examples: + + >>> g = Graph.Full_Bipartite(10, 5) + >>> g1, g2 = g.bipartite_projection() + >>> g1.isomorphic(Graph.Full(10)) + True + >>> g2.isomorphic(Graph.Full(5)) + True + + @param types: an igraph vector containing the vertex types, or an + attribute name. Anything that evalulates to C{False} corresponds to + vertices of the first kind, everything else to the second kind. + @param multiplicity: if C{True}, then igraph keeps the multiplicity of + the edges in the projection in an edge attribute called C{"weight"}. + E.g., if there is an A-C-B and an A-D-B triplet in the bipartite + graph and there is no other X (apart from X=B and X=D) for which an + A-X-B triplet would exist in the bipartite graph, the multiplicity + of the A-B edge in the projection will be 2. + @param probe1: this argument can be used to specify the order of the + projections in the resulting list. If given and non-negative, then + it is considered as a vertex ID; the projection containing the + vertex will be the first one in the result. + @param which: this argument can be used to specify which of the two + projections should be returned if only one of them is needed. Passing + 0 here means that only the first projection is returned, while 1 means + that only the second projection is returned. (Note that we use 0 and 1 + because Python indexing is zero-based). C{False} is equivalent to 0 and + C{True} is equivalent to 1. Any other value means that both projections + will be returned in a tuple. + @return: a tuple containing the two projected one-mode graphs if C{which} + is not 1 or 2, or the projected one-mode graph specified by the + C{which} argument if its value is 0, 1, C{False} or C{True}. + """ + # Deferred import to avoid cycles + from igraph import Graph + + superclass_meth = super(Graph, graph).bipartite_projection + + if which is False: + which = 0 + elif which is True: + which = 1 + if which != 0 and which != 1: + which = -1 + + if multiplicity: + if which == 0: + g1, w1 = superclass_meth(types, True, probe1, which) + g2, w2 = None, None + elif which == 1: + g1, w1 = None, None + g2, w2 = superclass_meth(types, True, probe1, which) + else: + g1, g2, w1, w2 = superclass_meth(types, True, probe1, which) + + if g1 is not None: + g1.es["weight"] = w1 + if g2 is not None: + g2.es["weight"] = w2 + return g1, g2 + else: + return g1 + else: + g2.es["weight"] = w2 + return g2 + else: + return superclass_meth(types, False, probe1, which) + + +def _bipartite_projection_size(graph, types="type", *args, **kwds): + """Calculates the number of vertices and edges in the bipartite + projections of this graph according to the specified vertex types. + This is useful if you have a bipartite graph and you want to estimate + the amount of memory you would need to calculate the projections + themselves. + + @param types: an igraph vector containing the vertex types, or an + attribute name. Anything that evalulates to C{False} corresponds to + vertices of the first kind, everything else to the second kind. + @return: a 4-tuple containing the number of vertices and edges in the + first projection, followed by the number of vertices and edges in the + second projection. + """ + # Deferred import to avoid cycles + from igraph import Graph + + return super(Graph, graph).bipartite_projection_size(types, *args, **kwds) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 92631d31c..72c7310fe 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -5,6 +5,7 @@ from copy import deepcopy from io import StringIO +from igraph._igraph import GraphBase from igraph import community_to_membership from igraph.configuration import Configuration from igraph.datatypes import UniqueIdGenerator @@ -1530,3 +1531,63 @@ def split_join_distance(comm1, comm2, remove_none=False): vec1, vec2 = _prepare_community_comparison(comm1, comm2, remove_none) return igraph._igraph._split_join_distance(vec1, vec2) + + +def _biconnected_components(graph, return_articulation_points=False): + """\ + Calculates the biconnected components of the graph. + + @param return_articulation_points: whether to return the articulation + points as well + @return: a L{VertexCover} object describing the biconnected components, + and optionally the list of articulation points as well + """ + if return_articulation_points: + trees, aps = GraphBase.biconnected_components(graph, True) + else: + trees = GraphBase.biconnected_components(graph, False) + + clusters = [] + if trees: + edgelist = graph.get_edgelist() + for tree in trees: + cluster = set() + for edge_id in tree: + cluster.update(edgelist[edge_id]) + clusters.append(sorted(cluster)) + + clustering = VertexCover(graph, clusters) + + if return_articulation_points: + return clustering, aps + else: + return clustering + + +def _cohesive_blocks(graph): + """Calculates the cohesive block structure of the graph. + + Cohesive blocking is a method of determining hierarchical subsets of graph + vertices based on their structural cohesion (i.e. vertex connectivity). + For a given graph G, a subset of its vertices S is said to be maximally + k-cohesive if there is no superset of S with vertex connectivity greater + than or equal to k. Cohesive blocking is a process through which, given a + k-cohesive set of vertices, maximally l-cohesive subsets are recursively + identified with l > k. Thus a hierarchy of vertex subsets is obtained in + the end, with the entire graph G at its root. + + @return: an instance of L{CohesiveBlocks}. See the documentation of + L{CohesiveBlocks} for more information. + @see: L{CohesiveBlocks} + """ + return CohesiveBlocks(graph, *GraphBase.cohesive_blocks(graph)) + + +def _clusters(graph, mode="strong"): + """Calculates the (strong or weak) clusters (connected components) for + a given graph. + + @param mode: must be either C{"strong"} or C{"weak"}, depending on the + clusters being sought. Optional, defaults to C{"strong"}. + @return: a L{VertexClustering} object""" + return VertexClustering(graph, GraphBase.clusters(graph, mode)) diff --git a/src/igraph/community/__init__.py b/src/igraph/community/__init__.py index cf5b56410..578dc4568 100644 --- a/src/igraph/community/__init__.py +++ b/src/igraph/community/__init__.py @@ -476,3 +476,37 @@ def _community_leiden( return VertexClustering(graph, membership, modularity_params=modularity_params) +def _modularity(self, membership, weights=None): + """Calculates the modularity score of the graph with respect to a given + clustering. + + The modularity of a graph w.r.t. some division measures how good the + division is, or how separated are the different vertex types from each + other. It's defined as M{Q=1/(2m)*sum(Aij-ki*kj/(2m)delta(ci,cj),i,j)}. + M{m} is the number of edges, M{Aij} is the element of the M{A} + adjacency matrix in row M{i} and column M{j}, M{ki} is the degree of + node M{i}, M{kj} is the degree of node M{j}, and M{Ci} and C{cj} are + the types of the two vertices (M{i} and M{j}). M{delta(x,y)} is one iff + M{x=y}, 0 otherwise. + + If edge weights are given, the definition of modularity is modified as + follows: M{Aij} becomes the weight of the corresponding edge, M{ki} + is the total weight of edges adjacent to vertex M{i}, M{kj} is the + total weight of edges adjacent to vertex M{j} and M{m} is the total + edge weight in the graph. + + @param membership: a membership list or a L{VertexClustering} object + @param weights: optional edge weights or C{None} if all edges are + weighed equally. Attribute names are also allowed. + @return: the modularity score + + @newfield ref: Reference + @ref: MEJ Newman and M Girvan: Finding and evaluating community + structure in networks. Phys Rev E 69 026113, 2004. + """ + if isinstance(membership, VertexClustering): + if membership.graph != self: + raise ValueError("clustering object belongs to another graph") + return GraphBase.modularity(self, membership.membership, weights) + else: + return GraphBase.modularity(self, membership, weights) diff --git a/src/igraph/components.py b/src/igraph/components.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/igraph/cut.py b/src/igraph/cut.py index c6be762d4..4cf62d937 100644 --- a/src/igraph/cut.py +++ b/src/igraph/cut.py @@ -2,6 +2,9 @@ # -*- coding: utf-8 -*- """Classes representing cuts and flows on graphs.""" +from igraph._igraph import ( + GraphBase, +) from igraph.clustering import VertexClustering @@ -187,3 +190,142 @@ def flow(self): larger vertex ID to the smaller. """ return self._flow + + +def _all_st_cuts(graph, source, target): + """\ + Returns all the cuts between the source and target vertices in a + directed graph. + + This function lists all edge-cuts between a source and a target vertex. + Every cut is listed exactly once. + + @param source: the source vertex ID + @param target: the target vertex ID + @return: a list of L{Cut} objects. + + @newfield ref: Reference + @ref: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in + graphs. Algorithmica 15, 351--372, 1996. + """ + return [ + Cut(graph, cut=cut, partition=part) + for cut, part in zip(*GraphBase.all_st_cuts(graph, source, target)) + ] + + +def _all_st_mincuts(graph, source, target, capacity=None): + """\ + Returns all the mincuts between the source and target vertices in a + directed graph. + + This function lists all minimum edge-cuts between a source and a target + vertex. + + @param source: the source vertex ID + @param target: the target vertex ID + @param capacity: the edge capacities (weights). If C{None}, all + edges have equal weight. May also be an attribute name. + @return: a list of L{Cut} objects. + + @newfield ref: Reference + @ref: JS Provan and DR Shier: A paradigm for listing (s,t)-cuts in + graphs. Algorithmica 15, 351--372, 1996. + """ + value, cuts, parts = GraphBase.all_st_mincuts(graph, source, target, capacity) + return [ + Cut(graph, value, cut=cut, partition=part) for cut, part in zip(cuts, parts) + ] + + +def _gomory_hu_tree(graph, capacity=None, flow="flow"): + """Calculates the Gomory-Hu tree of an undirected graph with optional + edge capacities. + + The Gomory-Hu tree is a concise representation of the value of all the + maximum flows (or minimum cuts) in a graph. The vertices of the tree + correspond exactly to the vertices of the original graph in the same order. + Edges of the Gomory-Hu tree are annotated by flow values. The value of + the maximum flow (or minimum cut) between an arbitrary (u,v) vertex + pair in the original graph is then given by the minimum flow value (i.e. + edge annotation) along the shortest path between u and v in the + Gomory-Hu tree. + + @param capacity: the edge capacities (weights). If C{None}, all + edges have equal weight. May also be an attribute name. + @param flow: the name of the edge attribute in the returned graph + in which the flow values will be stored. + @return: the Gomory-Hu tree as a L{Graph} object. + """ + graph, flow_values = GraphBase.gomory_hu_tree(graph, capacity) + graph.es[flow] = flow_values + return graph + + +def _maxflow(graph, source, target, capacity=None): + """Returns a maximum flow between the given source and target vertices + in a graph. + + A maximum flow from I{source} to I{target} is an assignment of + non-negative real numbers to the edges of the graph, satisfying + two properties: + + 1. For each edge, the flow (i.e. the assigned number) is not + more than the capacity of the edge (see the I{capacity} + argument) + + 2. For every vertex except the source and the target, the + incoming flow is the same as the outgoing flow. + + The value of the flow is the incoming flow of the target or the + outgoing flow of the source (which are equal). The maximum flow + is the maximum possible such value. + + @param capacity: the edge capacities (weights). If C{None}, all + edges have equal weight. May also be an attribute name. + @return: a L{Flow} object describing the maximum flow + """ + return Flow(graph, *GraphBase.maxflow(graph, source, target, capacity)) + + +def _mincut(graph, source=None, target=None, capacity=None): + """Calculates the minimum cut between the given source and target vertices + or within the whole graph. + + The minimum cut is the minimum set of edges that needs to be removed to + separate the source and the target (if they are given) or to disconnect the + graph (if neither the source nor the target are given). The minimum is + calculated using the weights (capacities) of the edges, so the cut with + the minimum total capacity is calculated. + + For undirected graphs and no source and target, the method uses the + Stoer-Wagner algorithm. For a given source and target, the method uses the + push-relabel algorithm; see the references below. + + @param source: the source vertex ID. If C{None}, the target must also be + C{None} and the calculation will be done for the entire graph (i.e. + all possible vertex pairs). + @param target: the target vertex ID. If C{None}, the source must also be + C{None} and the calculation will be done for the entire graph (i.e. + all possible vertex pairs). + @param capacity: the edge capacities (weights). If C{None}, all + edges have equal weight. May also be an attribute name. + @return: a L{Cut} object describing the minimum cut + """ + return Cut(graph, *GraphBase.mincut(graph, source, target, capacity)) + + +def _st_mincut(graph, source, target, capacity=None): + """Calculates the minimum cut between the source and target vertices in a + graph. + + @param source: the source vertex ID + @param target: the target vertex ID + @param capacity: the capacity of the edges. It must be a list or a valid + attribute name or C{None}. In the latter case, every edge will have the + same capacity. + @return: the value of the minimum cut, the IDs of vertices in the + first and second partition, and the IDs of edges in the cut, + packed in a 4-tuple + """ + return Cut(graph, *GraphBase.st_mincut(graph, source, target, capacity)) diff --git a/src/igraph/io/__init__.py b/src/igraph/io/__init__.py index e69de29bb..aae930b26 100644 --- a/src/igraph/io/__init__.py +++ b/src/igraph/io/__init__.py @@ -0,0 +1,25 @@ +_format_mapping = { + "ncol": ("Read_Ncol", "write_ncol"), + "lgl": ("Read_Lgl", "write_lgl"), + "graphdb": ("Read_GraphDB", None), + "graphmlz": ("Read_GraphMLz", "write_graphmlz"), + "graphml": ("Read_GraphML", "write_graphml"), + "gml": ("Read_GML", "write_gml"), + "dot": (None, "write_dot"), + "graphviz": (None, "write_dot"), + "net": ("Read_Pajek", "write_pajek"), + "pajek": ("Read_Pajek", "write_pajek"), + "dimacs": ("Read_DIMACS", "write_dimacs"), + "adjacency": ("Read_Adjacency", "write_adjacency"), + "adj": ("Read_Adjacency", "write_adjacency"), + "edgelist": ("Read_Edgelist", "write_edgelist"), + "edge": ("Read_Edgelist", "write_edgelist"), + "edges": ("Read_Edgelist", "write_edgelist"), + "pickle": ("Read_Pickle", "write_pickle"), + "picklez": ("Read_Picklez", "write_picklez"), + "svg": (None, "write_svg"), + "gw": (None, "write_leda"), + "leda": (None, "write_leda"), + "lgr": (None, "write_leda"), + "dl": ("Read_DL", None), +} diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 7ef002f1b..79ecced3a 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -21,6 +21,7 @@ '_layout_sugiyama', '_layout_method_wrapper', '_3d_version_for', + '_layout_mapping', ) @@ -716,3 +717,36 @@ def result(*args, **kwds): ) return result + +# After adjusting something here, don't forget to update the docstring +# of Graph.layout if necessary! +_layout_mapping = { + "auto": "layout_auto", + "automatic": "layout_auto", + "bipartite": "layout_bipartite", + "circle": "layout_circle", + "circular": "layout_circle", + "davidson_harel": "layout_davidson_harel", + "dh": "layout_davidson_harel", + "drl": "layout_drl", + "fr": "layout_fruchterman_reingold", + "fruchterman_reingold": "layout_fruchterman_reingold", + "graphopt": "layout_graphopt", + "grid": "layout_grid", + "kk": "layout_kamada_kawai", + "kamada_kawai": "layout_kamada_kawai", + "lgl": "layout_lgl", + "large": "layout_lgl", + "large_graph": "layout_lgl", + "mds": "layout_mds", + "random": "layout_random", + "rt": "layout_reingold_tilford", + "tree": "layout_reingold_tilford", + "reingold_tilford": "layout_reingold_tilford", + "rt_circular": "layout_reingold_tilford_circular", + "reingold_tilford_circular": "layout_reingold_tilford_circular", + "sphere": "layout_sphere", + "spherical": "layout_sphere", + "star": "layout_star", + "sugiyama": "layout_sugiyama", +} diff --git a/src/igraph/seq/__init__.py b/src/igraph/seq/__init__.py index de065bb79..eee305181 100644 --- a/src/igraph/seq/__init__.py +++ b/src/igraph/seq/__init__.py @@ -698,3 +698,107 @@ def __call__(self, *args, **kwds): return self.select(*args, **kwds) +def _graphmethod(func=None, name=None): + """Auxiliary decorator + + This decorator allows some methods of L{VertexSeq} and L{EdgeSeq} to + call their respective counterparts in L{Graph} to avoid code duplication. + + @param func: the function being decorated. This function will be + called on the results of the original L{Graph} method. + If C{None}, defaults to the identity function. + @param name: the name of the corresponding method in L{Graph}. If + C{None}, it defaults to the name of the decorated function. + @return: the decorated function + """ + # Delay import to avoid cycles + from igraph import Graph + + if name is None: + name = func.__name__ + method = getattr(Graph, name) + + if hasattr(func, "__call__"): + + def decorated(*args, **kwds): + self = args[0].graph + return func(args[0], method(self, *args, **kwds)) + + else: + + def decorated(*args, **kwds): + self = args[0].graph + return method(self, *args, **kwds) + + decorated.__name__ = name + decorated.__doc__ = """Proxy method to L{Graph.%(name)s()} + +This method calls the C{%(name)s()} method of the L{Graph} class +restricted to this sequence, and returns the result. + +@see: Graph.%(name)s() for details. +""" % { + "name": name + } + + return decorated + + +def _add_proxy_methods(): + + # Proxy methods for VertexSeq and EdgeSeq that forward their arguments to + # the corresponding Graph method are constructed here. Proxy methods for + # Vertex and Edge are added in the C source code. Make sure that you update + # the C source whenever you add a proxy method here if that makes sense for + # an individual vertex or edge + decorated_methods = {} + decorated_methods[VertexSeq] = [ + "degree", + "betweenness", + "bibcoupling", + "closeness", + "cocitation", + "constraint", + "diversity", + "eccentricity", + "get_shortest_paths", + "maxdegree", + "pagerank", + "personalized_pagerank", + "shortest_paths", + "similarity_dice", + "similarity_jaccard", + "subgraph", + "indegree", + "outdegree", + "isoclass", + "delete_vertices", + "is_separator", + "is_minimal_separator", + ] + decorated_methods[EdgeSeq] = [ + "count_multiple", + "delete_edges", + "is_loop", + "is_multiple", + "is_mutual", + "subgraph_edges", + ] + + rename_methods = {} + rename_methods[VertexSeq] = {"delete_vertices": "delete"} + rename_methods[EdgeSeq] = {"delete_edges": "delete", "subgraph_edges": "subgraph"} + + for cls, methods in decorated_methods.items(): + for method in methods: + new_method_name = rename_methods[cls].get(method, method) + setattr(cls, new_method_name, _graphmethod(None, method)) + + setattr( + EdgeSeq, + "edge_betweenness", + _graphmethod( + lambda self, result: [result[i] for i in self.indices], "edge_betweenness" + ), + ) + diff --git a/src/igraph/seq/edgeseq.py b/src/igraph/seq/edgeseq.py deleted file mode 100644 index 8ab03e2c2..000000000 --- a/src/igraph/seq/edgeseq.py +++ /dev/null @@ -1,431 +0,0 @@ -import operator - -from igraph._igraph import EdgeSeq as _EdgeSeq - - -class EdgeSeq(_EdgeSeq): - """Class representing a sequence of edges in the graph. - - This class is most easily accessed by the C{es} field of the - L{Graph} object, which returns an ordered sequence of all edges in - the graph. The edge sequence can be refined by invoking the - L{EdgeSeq.select()} method. L{EdgeSeq.select()} can also be - accessed by simply calling the L{EdgeSeq} object. - - An alternative way to create an edge sequence referring to a given - graph is to use the constructor directly: - - >>> g = Graph.Full(3) - >>> es = EdgeSeq(g) - >>> restricted_es = EdgeSeq(g, [0, 1]) - - The individual edges can be accessed by indexing the edge sequence - object. It can be used as an iterable as well, or even in a list - comprehension: - - >>> g=Graph.Full(3) - >>> for e in g.es: - ... print(e.tuple) - ... - (0, 1) - (0, 2) - (1, 2) - >>> [max(e.tuple) for e in g.es] - [1, 2, 2] - - The edge sequence can also be used as a dictionary where the keys are the - attribute names. The values corresponding to the keys are the values - of the given attribute of every edge in the graph: - - >>> g=Graph.Full(3) - >>> for idx, e in enumerate(g.es): - ... e["weight"] = idx*(idx+1) - ... - >>> g.es["weight"] - [0, 2, 6] - >>> g.es["weight"] = range(3) - >>> g.es["weight"] - [0, 1, 2] - - If you specify a sequence that is shorter than the number of edges in - the EdgeSeq, the sequence is reused: - - >>> g = Graph.Tree(7, 2) - >>> g.es["color"] = ["red", "green"] - >>> g.es["color"] - ['red', 'green', 'red', 'green', 'red', 'green'] - - You can even pass a single string or integer, it will be considered as a - sequence of length 1: - - >>> g.es["color"] = "red" - >>> g.es["color"] - ['red', 'red', 'red', 'red', 'red', 'red'] - - Some methods of the edge sequences are simply proxy methods to the - corresponding methods in the L{Graph} object. One such example is - C{EdgeSeq.is_multiple()}: - - >>> g=Graph(3, [(0,1), (1,0), (1,2)]) - >>> g.es.is_multiple() - [False, True, False] - >>> g.es.is_multiple() == g.is_multiple() - True - """ - - def attributes(self): - """Returns the list of all the edge attributes in the graph - associated to this edge sequence.""" - return self.graph.edge_attributes() - - def find(self, *args, **kwds): - """Returns the first edge of the edge sequence that matches some - criteria. - - The selection criteria are equal to the ones allowed by L{VertexSeq.select}. - See L{VertexSeq.select} for more details. - - For instance, to find the first edge with weight larger than 5 in graph C{g}: - - >>> g.es.find(weight_gt=5) #doctest:+SKIP - """ - if args: - # Selecting first based on positional arguments, then checking - # the criteria specified by the keyword arguments - edge = _EdgeSeq.find(self, *args) - if not kwds: - return edge - es = self.graph.es.select(edge.index) - else: - es = self - - # Selecting based on positional arguments - es = es.select(**kwds) - if es: - return es[0] - raise ValueError("no such edge") - - def select(self, *args, **kwds): - """Selects a subset of the edge sequence based on some criteria - - The selection criteria can be specified by the positional and the - keyword arguments. Positional arguments are always processed before - keyword arguments. - - - If the first positional argument is C{None}, an empty sequence is - returned. - - - If the first positional argument is a callable object, the object - will be called for every edge in the sequence. If it returns - C{True}, the edge will be included, otherwise it will - be excluded. - - - If the first positional argument is an iterable, it must return - integers and they will be considered as indices of the current - edge set (NOT the whole edge set of the graph -- the - difference matters when one filters an edge set that has - already been filtered by a previous invocation of - L{EdgeSeq.select()}. In this case, the indices do not refer - directly to the edges of the graph but to the elements of - the filtered edge sequence. - - - If the first positional argument is an integer, all remaining - arguments are expected to be integers. They are considered as - indices of the current edge set again. - - Keyword arguments can be used to filter the edges based on their - attributes and properties. The name of the keyword specifies the name - of the attribute and the filtering operator, they should be - concatenated by an underscore (C{_}) character. Attribute names can - also contain underscores, but operator names don't, so the operator is - always the largest trailing substring of the keyword name that does not - contain an underscore. Possible operators are: - - - C{eq}: equal to - - - C{ne}: not equal to - - - C{lt}: less than - - - C{gt}: greater than - - - C{le}: less than or equal to - - - C{ge}: greater than or equal to - - - C{in}: checks if the value of an attribute is in a given list - - - C{notin}: checks if the value of an attribute is not in a given - list - - For instance, if you want to filter edges with a numeric C{weight} - property larger than 50, you have to write: - - >>> g.es.select(weight_gt=50) #doctest: +SKIP - - Similarly, to filter edges whose C{type} is in a list of predefined - types: - - >>> list_of_types = ["inhibitory", "excitatory"] - >>> g.es.select(type_in=list_of_types) #doctest: +SKIP - - If the operator is omitted, it defaults to C{eq}. For instance, the - following selector selects edges whose C{type} property is - C{intracluster}: - - >>> g.es.select(type="intracluster") #doctest: +SKIP - - In the case of an unknown operator, it is assumed that the - recognized operator is part of the attribute name and the actual - operator is C{eq}. - - Keyword arguments are treated specially if they start with an - underscore (C{_}). These are not real attributes but refer to specific - properties of the edges, e.g., their centrality. The rules are as - follows: - - 1. C{_source} or {_from} means the source vertex of an edge. For - undirected graphs, only the C{eq} operator is supported and it - is treated as {_incident} (since undirected graphs have no notion - of edge directionality). - - 2. C{_target} or {_to} means the target vertex of an edge. For - undirected graphs, only the C{eq} operator is supported and it - is treated as {_incident} (since undirected graphs have no notion - of edge directionality). - - 3. C{_within} ignores the operator and checks whether both endpoints - of the edge lie within a specified set. - - 4. C{_between} ignores the operator and checks whether I{one} - endpoint of the edge lies within a specified set and the I{other} - endpoint lies within another specified set. The two sets must be - given as a tuple. - - 5. C{_incident} ignores the operator and checks whether the edge is - incident on a specific vertex or a set of vertices. - - 6. Otherwise, the rest of the name is interpreted as a method of the - L{Graph} object. This method is called with the edge sequence as - its first argument (all others left at default values) and edges - are filtered according to the value returned by the method. - - For instance, if you want to exclude edges with a betweenness - centrality less than 2: - - >>> g = Graph.Famous("zachary") - >>> excl = g.es.select(_edge_betweenness_ge = 2) - - To select edges originating from vertices 2 and 4: - - >>> edges = g.es.select(_source_in = [2, 4]) - - To select edges lying entirely within the subgraph spanned by vertices - 2, 3, 4 and 7: - - >>> edges = g.es.select(_within = [2, 3, 4, 7]) - - To select edges with one endpoint in the vertex set containing vertices - 2, 3, 4 and 7 and the other endpoint in the vertex set containing - vertices 8 and 9: - - >>> edges = g.es.select(_between = ([2, 3, 4, 7], [8, 9])) - - For properties that take a long time to be computed (e.g., betweenness - centrality for large graphs), it is advised to calculate the values - in advance and store it in a graph attribute. The same applies when - you are selecting based on the same property more than once in the - same C{select()} call to avoid calculating it twice unnecessarily. - For instance, the following would calculate betweenness centralities - twice: - - >>> edges = g.es.select(_edge_betweenness_gt=10, # doctest:+SKIP - ... _edge_betweenness_lt=30) - - It is advised to use this instead: - - >>> g.es["bs"] = g.edge_betweenness() - >>> edges = g.es.select(bs_gt=10, bs_lt=30) - - @return: the new, filtered edge sequence - """ - es = _EdgeSeq.select(self, *args) - is_directed = self.graph.is_directed() - - def _ensure_set(value): - if isinstance(value, VertexSeq): - value = set(v.index for v in value) - elif not isinstance(value, (set, frozenset)): - value = set(value) - return value - - operators = { - "lt": operator.lt, - "gt": operator.gt, - "le": operator.le, - "ge": operator.ge, - "eq": operator.eq, - "ne": operator.ne, - "in": lambda a, b: a in b, - "notin": lambda a, b: a not in b, - } - - # TODO(ntamas): some keyword arguments should be prioritized over - # others; for instance, we have optimized code paths for _source and - # _target in directed and undirected graphs if es.is_all() is True; - # these should be executed first. This matters only if there are - # multiple keyword arguments and es.is_all() is True. - - for keyword, value in kwds.items(): - if "_" not in keyword or keyword.rindex("_") == 0: - keyword = keyword + "_eq" - pos = keyword.rindex("_") - attr, op = keyword[0:pos], keyword[pos + 1 :] - try: - func = operators[op] - except KeyError: - # No such operator, assume that it's part of the attribute name - attr, op, func = keyword, "eq", operators["eq"] - - if attr[0] == "_": - if attr in ("_source", "_from", "_target", "_to") and not is_directed: - if op not in ("eq", "in"): - raise RuntimeError("unsupported for undirected graphs") - - # translate to _incident to avoid confusion - attr = "_incident" - if func == operators["eq"]: - if hasattr(value, "__iter__") and not isinstance(value, str): - value = set(value) - else: - value = set([value]) - - if attr in ("_source", "_from"): - if es.is_all() and op == "eq": - # shortcut here: use .incident() as it is much faster - filtered_idxs = sorted(es.graph.incident(value, mode="out")) - func = None - # TODO(ntamas): there are more possibilities; we could - # optimize "ne", "in" and "notin" in similar ways - else: - values = [e.source for e in es] - if op == "in" or op == "notin": - value = _ensure_set(value) - - elif attr in ("_target", "_to"): - if es.is_all() and op == "eq": - # shortcut here: use .incident() as it is much faster - filtered_idxs = sorted(es.graph.incident(value, mode="in")) - func = None - # TODO(ntamas): there are more possibilities; we could - # optimize "ne", "in" and "notin" in similar ways - else: - values = [e.target for e in es] - if op == "in" or op == "notin": - value = _ensure_set(value) - - elif attr == "_incident": - func = None # ignoring function, filtering here - value = _ensure_set(value) - - # Fetch all the edges that are incident on at least one of - # the vertices specified - candidates = set() - for v in value: - candidates.update(es.graph.incident(v)) - - if not es.is_all(): - # Find those that are in the current edge sequence - filtered_idxs = [ - i for i, e in enumerate(es) if e.index in candidates - ] - else: - # We are done, the filtered indexes are in the candidates set - filtered_idxs = sorted(candidates) - - elif attr == "_within": - func = None # ignoring function, filtering here - value = _ensure_set(value) - - # Fetch all the edges that are incident on at least one of - # the vertices specified - candidates = set() - for v in value: - candidates.update(es.graph.incident(v)) - - if not es.is_all(): - # Find those where both endpoints are OK - filtered_idxs = [ - i - for i, e in enumerate(es) - if e.index in candidates - and e.source in value - and e.target in value - ] - else: - # Optimized version when the edge sequence contains all - # the edges exactly once in increasing order of edge IDs - filtered_idxs = [ - i - for i in candidates - if es[i].source in value and es[i].target in value - ] - - elif attr == "_between": - if len(value) != 2: - raise ValueError( - "_between selector requires two vertex ID lists" - ) - func = None # ignoring function, filtering here - set1 = _ensure_set(value[0]) - set2 = _ensure_set(value[1]) - - # Fetch all the edges that are incident on at least one of - # the vertices specified - candidates = set() - for v in set1: - candidates.update(es.graph.incident(v)) - for v in set2: - candidates.update(es.graph.incident(v)) - - if not es.is_all(): - # Find those where both endpoints are OK - filtered_idxs = [ - i - for i, e in enumerate(es) - if (e.source in set1 and e.target in set2) - or (e.target in set1 and e.source in set2) - ] - else: - # Optimized version when the edge sequence contains all - # the edges exactly once in increasing order of edge IDs - filtered_idxs = [ - i - for i in candidates - if (es[i].source in set1 and es[i].target in set2) - or (es[i].target in set1 and es[i].source in set2) - ] - - else: - # Method call, not an attribute - values = getattr(es.graph, attr[1:])(es) - else: - values = es[attr] - - # If we have a function to apply on the values, do that; otherwise - # we assume that filtered_idxs has already been calculated. - if func is not None: - filtered_idxs = [i for i, v in enumerate(values) if func(v, value)] - - es = es.select(filtered_idxs) - - return es - - def __call__(self, *args, **kwds): - """Shorthand notation to select() - - This method simply passes all its arguments to L{EdgeSeq.select()}. - """ - return self.select(*args, **kwds) - - diff --git a/src/igraph/structural.py b/src/igraph/structural.py new file mode 100644 index 000000000..eb7e38c75 --- /dev/null +++ b/src/igraph/structural.py @@ -0,0 +1,95 @@ +from igraph._igraph import ( + IN, + OUT, + arpack_options as default_arpack_options, +) +from igraph.statistics import Histogram + + +def _indegree(self, *args, **kwds): + """Returns the in-degrees in a list. + + See L{degree} for possible arguments. + """ + kwds["mode"] = IN + return self.degree(*args, **kwds) + + +def _outdegree(self, *args, **kwds): + """Returns the out-degrees in a list. + + See L{degree} for possible arguments. + """ + kwds["mode"] = OUT + return self.degree(*args, **kwds) + + +def _degree_distribution(self, bin_width=1, *args, **kwds): + """Calculates the degree distribution of the graph. + + Unknown keyword arguments are directly passed to L{degree()}. + + @param bin_width: the bin width of the histogram + @return: a histogram representing the degree distribution of the + graph. + """ + result = Histogram(bin_width, self.degree(*args, **kwds)) + return result + + +def _pagerank( + self, + vertices=None, + directed=True, + damping=0.85, + weights=None, + arpack_options=None, + implementation="prpack", + niter=1000, + eps=0.001, +): + """Calculates the PageRank values of a graph. + + @param vertices: the indices of the vertices being queried. + C{None} means all of the vertices. + @param directed: whether to consider directed paths. + @param damping: the damping factor. M{1-damping} is the PageRank value + for nodes with no incoming links. It is also the probability of + resetting the random walk to a uniform distribution in each step. + @param weights: edge weights to be used. Can be a sequence or iterable + or even an edge attribute name. + @param arpack_options: an L{ARPACKOptions} object used to fine-tune + the ARPACK eigenvector calculation. If omitted, the module-level + variable called C{arpack_options} is used. This argument is + ignored if not the ARPACK implementation is used, see the + I{implementation} argument. + @param implementation: which implementation to use to solve the + PageRank eigenproblem. Possible values are: + - C{"prpack"}: use the PRPACK library. This is a new + implementation in igraph 0.7 + - C{"arpack"}: use the ARPACK library. This implementation + was used from version 0.5, until version 0.7. + - C{"power"}: use a simple power method. This is the + implementation that was used before igraph version 0.5. + @param niter: The number of iterations to use in the power method + implementation. It is ignored in the other implementations + @param eps: The power method implementation will consider the + calculation as complete if the difference of PageRank values between + iterations change less than this value for every node. It is + ignored by the other implementations. + @return: a list with the Google PageRank values of the specified + vertices.""" + if arpack_options is None: + arpack_options = default_arpack_options + return self.personalized_pagerank( + vertices, + directed, + damping, + None, + None, + weights, + arpack_options, + implementation, + niter, + eps, + ) From 679f3525017c236bd4540e84b8dde16648005d88 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Tue, 5 Oct 2021 09:15:08 +1100 Subject: [PATCH 0475/1681] self -> graph in structural.py --- src/igraph/structural.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/igraph/structural.py b/src/igraph/structural.py index eb7e38c75..f751adf20 100644 --- a/src/igraph/structural.py +++ b/src/igraph/structural.py @@ -6,25 +6,25 @@ from igraph.statistics import Histogram -def _indegree(self, *args, **kwds): +def _indegree(graph, *args, **kwds): """Returns the in-degrees in a list. See L{degree} for possible arguments. """ kwds["mode"] = IN - return self.degree(*args, **kwds) + return graph.degree(*args, **kwds) -def _outdegree(self, *args, **kwds): +def _outdegree(graph, *args, **kwds): """Returns the out-degrees in a list. See L{degree} for possible arguments. """ kwds["mode"] = OUT - return self.degree(*args, **kwds) + return graph.degree(*args, **kwds) -def _degree_distribution(self, bin_width=1, *args, **kwds): +def _degree_distribution(graph, bin_width=1, *args, **kwds): """Calculates the degree distribution of the graph. Unknown keyword arguments are directly passed to L{degree()}. @@ -33,12 +33,12 @@ def _degree_distribution(self, bin_width=1, *args, **kwds): @return: a histogram representing the degree distribution of the graph. """ - result = Histogram(bin_width, self.degree(*args, **kwds)) + result = Histogram(bin_width, graph.degree(*args, **kwds)) return result def _pagerank( - self, + graph, vertices=None, directed=True, damping=0.85, @@ -81,7 +81,7 @@ def _pagerank( vertices.""" if arpack_options is None: arpack_options = default_arpack_options - return self.personalized_pagerank( + return graph.personalized_pagerank( vertices, directed, damping, From b93b1f15aa615e0b87eb3bbee874a7d6c99a3827 Mon Sep 17 00:00:00 2001 From: Fabio Zanini Date: Mon, 18 Oct 2021 16:18:13 +1100 Subject: [PATCH 0476/1681] Move module __init__ files into main modules --- src/igraph/{basic/__init__.py => basic.py} | 0 src/igraph/{community/__init__.py => community.py} | 0 src/igraph/{seq/__init__.py => seq.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/igraph/{basic/__init__.py => basic.py} (100%) rename src/igraph/{community/__init__.py => community.py} (100%) rename src/igraph/{seq/__init__.py => seq.py} (100%) diff --git a/src/igraph/basic/__init__.py b/src/igraph/basic.py similarity index 100% rename from src/igraph/basic/__init__.py rename to src/igraph/basic.py diff --git a/src/igraph/community/__init__.py b/src/igraph/community.py similarity index 100% rename from src/igraph/community/__init__.py rename to src/igraph/community.py diff --git a/src/igraph/seq/__init__.py b/src/igraph/seq.py similarity index 100% rename from src/igraph/seq/__init__.py rename to src/igraph/seq.py From 34f02b90c294d5ae2adeeb4d47b83f2564c6608c Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 15:04:49 +0200 Subject: [PATCH 0477/1681] chore: reformatted code with black --- src/igraph/__init__.py | 20 ++++++++++---------- src/igraph/adjacency.py | 10 +++++----- src/igraph/automorphisms.py | 5 ++--- src/igraph/basic.py | 1 + src/igraph/clustering.py | 22 +++++++++------------- src/igraph/community.py | 4 +--- src/igraph/io/objects.py | 15 ++++++++++----- src/igraph/layout.py | 14 +++++++------- src/igraph/operators/__init__.py | 22 +++++++++++----------- src/igraph/seq.py | 3 +-- 10 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index a8c9fc59e..9ad3fde19 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -613,16 +613,16 @@ def es(self): ################### # Graph operators - __iadd__ = _operator_method_registry['__iadd__'] - __add__ = _operator_method_registry['__add__'] - __and__ = _operator_method_registry['__and__'] - __isub__ = _operator_method_registry['__isub__'] - __sub__ = _operator_method_registry['__sub__'] - __mul__ = _operator_method_registry['__mul__'] - __or__ = _operator_method_registry['__or__'] - disjoint_union = _operator_method_registry['disjoint_union'] - union = _operator_method_registry['union'] - intersection = _operator_method_registry['intersection'] + __iadd__ = _operator_method_registry["__iadd__"] + __add__ = _operator_method_registry["__add__"] + __and__ = _operator_method_registry["__and__"] + __isub__ = _operator_method_registry["__isub__"] + __sub__ = _operator_method_registry["__sub__"] + __mul__ = _operator_method_registry["__mul__"] + __or__ = _operator_method_registry["__or__"] + disjoint_union = _operator_method_registry["disjoint_union"] + union = _operator_method_registry["union"] + intersection = _operator_method_registry["intersection"] ############################################# # Adjacency/incidence diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py index 65ae88606..94fbd2822 100644 --- a/src/igraph/adjacency.py +++ b/src/igraph/adjacency.py @@ -15,11 +15,11 @@ __all__ = ( - '_get_adjacency', - '_get_adjacency_sparse', - '_get_adjlist', - '_get_incidence', - '_get_inclist', + "_get_adjacency", + "_get_adjacency_sparse", + "_get_adjlist", + "_get_incidence", + "_get_inclist", ) diff --git a/src/igraph/automorphisms.py b/src/igraph/automorphisms.py index 84178e9cf..6c8b5a417 100644 --- a/src/igraph/automorphisms.py +++ b/src/igraph/automorphisms.py @@ -1,6 +1,6 @@ __all__ = ( - '_count_automorphisms_vf2', - '_get_automorphisms_vf2', + "_count_automorphisms_vf2", + "_get_automorphisms_vf2", ) @@ -49,4 +49,3 @@ def _get_automorphisms_vf2( node_compat_fn=node_compat_fn, edge_compat_fn=edge_compat_fn, ) - diff --git a/src/igraph/basic.py b/src/igraph/basic.py index f02006a63..a0ce23abd 100644 --- a/src/igraph/basic.py +++ b/src/igraph/basic.py @@ -162,6 +162,7 @@ def _as_directed(graph, *args, **kwds): copy.to_directed(*args, **kwds) return copy + def _as_undirected(graph, *args, **kwds): """Returns an undirected copy of this graph. Arguments are passed on to L{to_undirected()} that is invoked on the copy. diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index 72c7310fe..c302a3d68 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -485,9 +485,9 @@ def __plot__(self, backend, context, *args, **kwds): colors[is_crossing] for is_crossing in self.crossing() ] - palette = kwds.get('palette', None) + palette = kwds.get("palette", None) if palette is None: - kwds['palette'] = ClusterColoringPalette(len(self)) + kwds["palette"] = ClusterColoringPalette(len(self)) if "mark_groups" not in kwds: if Configuration.instance()["plotting.mark_groups"]: @@ -745,12 +745,12 @@ def __plot__(self, backend, context, *args, **kwds): if backend == "matplotlib": drawer = MatplotlibDendrogramDrawer(context) else: - bbox = kwds.pop('bbox', None) - palette = kwds.pop('palette', None) + bbox = kwds.pop("bbox", None) + palette = kwds.pop("palette", None) if bbox is None: - raise ValueError('bbox is required for cairo plots') + raise ValueError("bbox is required for cairo plots") if palette is None: - raise ValueError('palette is required for cairo plots') + raise ValueError("palette is required for cairo plots") drawer = CairoDendrogramDrawer(context, bbox, palette) drawer.draw(self, **kwds) @@ -878,9 +878,7 @@ class VisualVertexBuilder(AttributeCollectorBase): name if name is not None else str(idx) for idx, name in enumerate(self._names) ] - result = Dendrogram.__plot__( - self, backend, context, *args, **kwds - ) + result = Dendrogram.__plot__(self, backend, context, *args, **kwds) del self._names return result @@ -1176,7 +1174,7 @@ def __plot__(self, backend, context, *args, **kwds): """ if "edge_color" not in kwds and "color" not in self.graph.edge_attributes(): # Set up a default edge coloring based on internal vs external edges - if backend == 'matplotlib': + if backend == "matplotlib": colors = ["dimgrey", "silver"] else: colors = ["grey20", "grey80"] @@ -1330,9 +1328,7 @@ def __plot__(self, backend, context, *args, **kwds): if "vertex_color" not in kwds: kwds["vertex_color"] = self.max_cohesions() - return VertexCover.__plot__( - self, backend, context, *args, **kwds - ) + return VertexCover.__plot__(self, backend, context, *args, **kwds) def _handle_mark_groups_arg_for_clustering(mark_groups, clustering): diff --git a/src/igraph/community.py b/src/igraph/community.py index 578dc4568..644501577 100644 --- a/src/igraph/community.py +++ b/src/igraph/community.py @@ -138,9 +138,7 @@ def _community_leading_eigenvector( if arpack_options is not None: kwds["arpack_options"] = arpack_options - membership, _, q = GraphBase.community_leading_eigenvector( - graph, clusters, **kwds - ) + membership, _, q = GraphBase.community_leading_eigenvector(graph, clusters, **kwds) return VertexClustering(graph, membership, modularity=q) diff --git a/src/igraph/io/objects.py b/src/igraph/io/objects.py index 8300e1e03..0864fe3b7 100644 --- a/src/igraph/io/objects.py +++ b/src/igraph/io/objects.py @@ -80,7 +80,7 @@ def create_list_from_indices(indices, n): if vertex_name_attr not in vertex_attrs: raise AttributeError( - f'{vertex_name_attr} is not a key of your vertex dictionaries', + f"{vertex_name_attr} is not a key of your vertex dictionaries", ) vertex_names = vertex_attrs[vertex_name_attr] @@ -639,7 +639,10 @@ def _export_graph_to_tuple_list( def _export_graph_to_list_dict( - graph, use_vids=True, sequence_constructor=list, vertex_name_attr="name", + graph, + use_vids=True, + sequence_constructor=list, + vertex_name_attr="name", ): """Export graph to a dictionary of lists (or other sequences). @@ -671,7 +674,7 @@ def _export_graph_to_list_dict( """ if not use_vids: if vertex_name_attr not in graph.vertex_attributes(): - raise AttributeError(f'Vertices do not have a {vertex_name_attr} attribute') + raise AttributeError(f"Vertices do not have a {vertex_name_attr} attribute") vs_names = graph.vs[vertex_name_attr] # Temporary output data structure @@ -690,7 +693,9 @@ def _export_graph_to_list_dict( return res -def _export_graph_to_dict_dict(graph, use_vids=True, edge_attrs=None, skip_none=False, vertex_name_attr="name"): +def _export_graph_to_dict_dict( + graph, use_vids=True, edge_attrs=None, skip_none=False, vertex_name_attr="name" +): """Export graph to dictionary of dicts of edge attributes This function is the reverse of Graph.DictDict. @@ -730,7 +735,7 @@ def _export_graph_to_dict_dict(graph, use_vids=True, edge_attrs=None, skip_none= if not use_vids: if vertex_name_attr not in graph.vertex_attributes(): - raise AttributeError(f'Vertices do not have a {vertex_name_attr} attribute') + raise AttributeError(f"Vertices do not have a {vertex_name_attr} attribute") vs_names = graph.vs[vertex_name_attr] # Temporary output data structure diff --git a/src/igraph/layout.py b/src/igraph/layout.py index 79ecced3a..7f02349ff 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -15,13 +15,13 @@ __all__ = ( - 'Layout', - '_layout', - '_layout_auto', - '_layout_sugiyama', - '_layout_method_wrapper', - '_3d_version_for', - '_layout_mapping', + "Layout", + "_layout", + "_layout_auto", + "_layout_sugiyama", + "_layout_method_wrapper", + "_3d_version_for", + "_layout_mapping", ) diff --git a/src/igraph/operators/__init__.py b/src/igraph/operators/__init__.py index 0b6e86291..b2db4c2b7 100644 --- a/src/igraph/operators/__init__.py +++ b/src/igraph/operators/__init__.py @@ -7,7 +7,7 @@ "union", "intersection", "operator_method_registry", - ) +) from igraph.operators.functions import ( disjoint_union, @@ -28,14 +28,14 @@ ) operator_method_registry = { - '__iadd__': __iadd__, - '__add__': __add__, - '__and__': __and__, - '__isub__': __isub__, - '__sub__': __sub__, - '__mul__': __mul__, - '__or__': __or__, - 'disjoint_union': _disjoint_union, - 'union': _union, - 'intersection': _intersection, + "__iadd__": __iadd__, + "__add__": __add__, + "__and__": __and__, + "__isub__": __isub__, + "__sub__": __sub__, + "__mul__": __mul__, + "__or__": __or__, + "disjoint_union": _disjoint_union, + "union": _union, + "intersection": _intersection, } diff --git a/src/igraph/seq.py b/src/igraph/seq.py index eee305181..4d32e3c02 100644 --- a/src/igraph/seq.py +++ b/src/igraph/seq.py @@ -3,7 +3,7 @@ from igraph._igraph import ( EdgeSeq as _EdgeSeq, VertexSeq as _VertexSeq, -) +) class VertexSeq(_VertexSeq): @@ -801,4 +801,3 @@ def _add_proxy_methods(): lambda self, result: [result[i] for i in self.indices], "edge_betweenness" ), ) - From 56d97278f0dfaa4681e0093eef3c0b43dc143c72 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 15:05:30 +0200 Subject: [PATCH 0478/1681] chore: removed empty components.py --- src/igraph/components.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/igraph/components.py diff --git a/src/igraph/components.py b/src/igraph/components.py deleted file mode 100644 index e69de29bb..000000000 From 9b401abf767f79942c8209ecb8c809d05d4f6098 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 16:33:47 +0200 Subject: [PATCH 0479/1681] style: fixed a few linter warnings --- src/igraph/__init__.py | 3 --- src/igraph/adjacency.py | 7 ------- src/igraph/configuration.py | 1 - src/igraph/io/objects.py | 5 +---- src/igraph/utils.py | 1 - 5 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 9ad3fde19..8cb6a9955 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -270,10 +270,7 @@ import os import sys -import operator -from collections import defaultdict -from shutil import copyfileobj from warnings import warn diff --git a/src/igraph/adjacency.py b/src/igraph/adjacency.py index 94fbd2822..48d35fe38 100644 --- a/src/igraph/adjacency.py +++ b/src/igraph/adjacency.py @@ -1,11 +1,4 @@ from igraph._igraph import ( - ADJ_DIRECTED, - ADJ_LOWER, - ADJ_MAX, - ADJ_MIN, - ADJ_PLUS, - ADJ_UNDIRECTED, - ADJ_UPPER, GET_ADJACENCY_BOTH, GET_ADJACENCY_LOWER, GET_ADJACENCY_UPPER, diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 758c4adfd..1ae5bd016 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -9,7 +9,6 @@ """ import os.path -import platform from configparser import ConfigParser diff --git a/src/igraph/io/objects.py b/src/igraph/io/objects.py index 0864fe3b7..430f81c70 100644 --- a/src/igraph/io/objects.py +++ b/src/igraph/io/objects.py @@ -401,16 +401,13 @@ def _construct_graph_from_dataframe( to unexpected behaviour: fill your NaNs with values before calling this function to mitigate. """ - # Deferred import to avoid cycles - from igraph import Graph - try: import pandas as pd except ImportError: raise ImportError("You should install pandas in order to use this function") try: import numpy as np - except: + except ImportError: raise ImportError("You should install numpy in order to use this function") if edges.shape[1] < 2: diff --git a/src/igraph/utils.py b/src/igraph/utils.py index f919ef717..ec36f8764 100644 --- a/src/igraph/utils.py +++ b/src/igraph/utils.py @@ -5,7 +5,6 @@ from contextlib import contextmanager from collections.abc import MutableMapping -from ctypes import sizeof from itertools import chain import os From e431cc8ba5c29c45d7fbb1cea589767d165680d4 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 16:37:02 +0200 Subject: [PATCH 0480/1681] ci: restrict Pillow to <8.4 --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 7a31ed1ff..d2c36165a 100644 --- a/setup.py +++ b/setup.py @@ -841,6 +841,11 @@ def use_educated_guess(self) -> None: "scipy>=1.5.0; platform_python_implementation != 'PyPy'", "matplotlib>=3.3.4; platform_python_implementation != 'PyPy'", "plotly>=5.3.0", + # matplotlib requires Pillow; however, Pillow >= 8.4 does not + # provide manylinux2010 wheels any more, but we need those in + # cibuildwheel for Linux so we need to restrict Pillow's version + # range + "Pillow>=8,<8.4; platform_python_implementation != 'PyPy'", ], "doc": [ "Sphinx>=4.2.0", From cd63424ef759cafddae0650054ee7d470c298c7f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 17:37:00 +0200 Subject: [PATCH 0481/1681] chore: updated vendored igraph --- vendor/source/igraph | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/source/igraph b/vendor/source/igraph index 6f154c7ac..24b6ef417 160000 --- a/vendor/source/igraph +++ b/vendor/source/igraph @@ -1 +1 @@ -Subproject commit 6f154c7ac0946f7a0baf1ce403c520cacf6035d7 +Subproject commit 24b6ef417a2929a7b3bc2e95d9bd792213a80134 From bb3de00be2bdd53bfe570bb0d8b04cdd3cc1c11f Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 18:43:55 +0200 Subject: [PATCH 0482/1681] fix: reinstantiated Graph.write_svg() --- src/igraph/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 8cb6a9955..87df9bea5 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -485,6 +485,8 @@ def __init__(self, *args, **kwds): Read_Adjacency = classmethod(_construct_graph_from_adjacency_file) write_adjacency = _write_graph_to_adjacency_file + write_svg = _write_graph_to_svg + Read = classmethod(_construct_graph_from_file) Load = Read write = _write_graph_to_file From 20b3cfc211373838042764b4d95c45a15ca3a075 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Fri, 22 Oct 2021 18:48:05 +0200 Subject: [PATCH 0483/1681] style: blackened entire source --- src/igraph/clustering.py | 4 +- src/igraph/drawing/__init__.py | 43 +++++++-------- src/igraph/drawing/cairo/graph.py | 4 +- src/igraph/drawing/cairo/histogram.py | 5 +- src/igraph/drawing/cairo/palette.py | 3 +- src/igraph/drawing/cairo/text.py | 2 +- src/igraph/drawing/cairo/utils.py | 2 +- src/igraph/drawing/matplotlib/histogram.py | 2 +- src/igraph/drawing/matplotlib/palette.py | 2 +- src/igraph/drawing/matplotlib/utils.py | 2 +- src/igraph/drawing/plotly/edge.py | 62 +++++++++++++--------- src/igraph/drawing/plotly/graph.py | 27 +++++----- src/igraph/drawing/plotly/polygon.py | 40 ++++++++------ src/igraph/drawing/plotly/utils.py | 15 +++--- src/igraph/drawing/plotly/vertex.py | 31 +++++------ src/igraph/drawing/text.py | 2 +- 16 files changed, 131 insertions(+), 115 deletions(-) diff --git a/src/igraph/clustering.py b/src/igraph/clustering.py index c302a3d68..bc632bdcb 100644 --- a/src/igraph/clustering.py +++ b/src/igraph/clustering.py @@ -748,9 +748,9 @@ def __plot__(self, backend, context, *args, **kwds): bbox = kwds.pop("bbox", None) palette = kwds.pop("palette", None) if bbox is None: - raise ValueError("bbox is required for cairo plots") + raise ValueError("bbox is required for Cairo plots") if palette is None: - raise ValueError("palette is required for cairo plots") + raise ValueError("palette is required for Cairo plots") drawer = CairoDendrogramDrawer(context, bbox, palette) drawer.draw(self, **kwds) diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index a1f53d00d..67094084c 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -70,29 +70,29 @@ class DrawerDirectory: This directory is used by the __plot__ functions. """ - valid_backends = ('cairo', 'matplotlib') + + valid_backends = ("cairo", "matplotlib") valid_objects = ( - 'Graph', - 'Matrix', - 'Histogram', - 'Palette', + "Graph", + "Matrix", + "Histogram", + "Palette", ) known_drawers = { - 'cairo': { - 'Graph': CairoGraphDrawer, - 'Matrix': CairoMatrixDrawer, - 'Histogram': CairoHistogramDrawer, - 'Palette': CairoPaletteDrawer, - + "cairo": { + "Graph": CairoGraphDrawer, + "Matrix": CairoMatrixDrawer, + "Histogram": CairoHistogramDrawer, + "Palette": CairoPaletteDrawer, }, - 'matplotlib': { - 'Graph': MatplotlibGraphDrawer, - 'Matrix': MatplotlibMatrixDrawer, - 'Histogram': MatplotlibHistogramDrawer, - 'Palette': MatplotlibPaletteDrawer, + "matplotlib": { + "Graph": MatplotlibGraphDrawer, + "Matrix": MatplotlibMatrixDrawer, + "Histogram": MatplotlibHistogramDrawer, + "Palette": MatplotlibPaletteDrawer, }, - 'plotly': { - 'Graph': PlotlyGraphDrawer, + "plotly": { + "Graph": PlotlyGraphDrawer, }, } @@ -106,14 +106,14 @@ def resolve(cls, obj, backend): @raise ValueError: if no drawer is available for this backend/object """ - object_name = str(obj.__class__).split('.')[-1].strip("<'>") + object_name = str(obj.__class__).split(".")[-1].strip("<'>") try: return cls.known_drawers[backend][object_name] except KeyError: raise ValueError( f"unknown drawer for {object_name} and backend {backend}", - ) + ) def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): @@ -196,7 +196,8 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): if hasattr(plt, "Axes") and isinstance(target, plt.Axes): backend = "matplotlib" elif hasattr(plotly, "graph_objects") and isinstance( - target, plotly.graph_objects.Figure): + target, plotly.graph_objects.Figure + ): backend = "plotly" elif hasattr(cairo, "Surface") and isinstance(target, cairo.Surface): backend = "cairo" diff --git a/src/igraph/drawing/cairo/graph.py b/src/igraph/drawing/cairo/graph.py index d66ce7b36..44bbef880 100644 --- a/src/igraph/drawing/cairo/graph.py +++ b/src/igraph/drawing/cairo/graph.py @@ -122,9 +122,9 @@ def draw(self, graph, *args, **kwds): DeprecationWarning, ) - bbox = kwds.pop('bbox', None) + bbox = kwds.pop("bbox", None) if bbox is None: - raise ValueError('bbox is required for cairo plots') + raise ValueError("bbox is required for Cairo plots") # Validate it through set/get self.bbox = bbox bbox = self.bbox diff --git a/src/igraph/drawing/cairo/histogram.py b/src/igraph/drawing/cairo/histogram.py index de984fe8e..81de8b3bd 100644 --- a/src/igraph/drawing/cairo/histogram.py +++ b/src/igraph/drawing/cairo/histogram.py @@ -22,9 +22,9 @@ def draw(self, histogram, **kwds): context = self.context - bbox = self.bbox = kwds.pop('bbox', None) + bbox = self.bbox = kwds.pop("bbox", None) if bbox is None: - raise ValueError('bbox is required for cairo plots') + raise ValueError("bbox is required for Cairo plots") xmin = kwds.get("min", self._min) ymin = 0 @@ -56,4 +56,3 @@ def draw(self, histogram, **kwds): # Draw the axes coord_system.draw() - diff --git a/src/igraph/drawing/cairo/palette.py b/src/igraph/drawing/cairo/palette.py index 9c0e67a16..22180ae0e 100644 --- a/src/igraph/drawing/cairo/palette.py +++ b/src/igraph/drawing/cairo/palette.py @@ -41,7 +41,7 @@ def draw(self, palette, **kwds): grid_width = float(kwds.get("grid_width", 0.0)) return matrix.__plot__( - 'cairo', + "cairo", context, bbox=bbox, palette=self, @@ -50,4 +50,3 @@ def draw(self, palette, **kwds): grid_width=grid_width, border_width=border_width, ) - diff --git a/src/igraph/drawing/cairo/text.py b/src/igraph/drawing/cairo/text.py index cbf29e0f3..a075a9fa1 100644 --- a/src/igraph/drawing/cairo/text.py +++ b/src/igraph/drawing/cairo/text.py @@ -7,7 +7,7 @@ from igraph.drawing.cairo.base import AbstractCairoDrawer -__all__ = ("CairoTextDrawer", ) +__all__ = ("CairoTextDrawer",) class CairoTextDrawer(AbstractCairoDrawer): diff --git a/src/igraph/drawing/cairo/utils.py b/src/igraph/drawing/cairo/utils.py index 753652b83..5bc8fa819 100644 --- a/src/igraph/drawing/cairo/utils.py +++ b/src/igraph/drawing/cairo/utils.py @@ -1,7 +1,7 @@ from igraph.drawing.utils import FakeModule from typing import Any -__all__ = ("find_cairo", ) +__all__ = ("find_cairo",) def find_cairo() -> Any: diff --git a/src/igraph/drawing/matplotlib/histogram.py b/src/igraph/drawing/matplotlib/histogram.py index 0084875c1..8100ebf47 100644 --- a/src/igraph/drawing/matplotlib/histogram.py +++ b/src/igraph/drawing/matplotlib/histogram.py @@ -32,6 +32,6 @@ def draw(self, matrix, **kwds): x = [self._min + width * i for i, _ in enumerate(self._bins)] y = self._bins # Draw the boxes/bars - ax.bar(x, y, align='left') + ax.bar(x, y, align="left") ax.set_xlim(xmin, xmax) ax.set_ylim(ymin, ymax) diff --git a/src/igraph/drawing/matplotlib/palette.py b/src/igraph/drawing/matplotlib/palette.py index fb9fc3850..05f38a7f1 100644 --- a/src/igraph/drawing/matplotlib/palette.py +++ b/src/igraph/drawing/matplotlib/palette.py @@ -42,7 +42,7 @@ def draw(self, matrix, **kwds): [self.get(i) for i in range(self.length)], ) matrix.__plot__( - 'matplotlib', + "matplotlib", ax, cmap=cmap, **kwds, diff --git a/src/igraph/drawing/matplotlib/utils.py b/src/igraph/drawing/matplotlib/utils.py index 3c842c656..ab36f43f0 100644 --- a/src/igraph/drawing/matplotlib/utils.py +++ b/src/igraph/drawing/matplotlib/utils.py @@ -1,7 +1,7 @@ from igraph.drawing.utils import FakeModule from typing import Any -__all__ = ("find_matplotlib", ) +__all__ = ("find_matplotlib",) def find_matplotlib() -> Any: diff --git a/src/igraph/drawing/plotly/edge.py b/src/igraph/drawing/plotly/edge.py index 410b04d5f..f0e416188 100644 --- a/src/igraph/drawing/plotly/edge.py +++ b/src/igraph/drawing/plotly/edge.py @@ -4,8 +4,17 @@ from igraph.drawing.baseclasses import AbstractEdgeDrawer from igraph.drawing.metamagic import AttributeCollectorBase -from igraph.drawing.plotly.utils import find_plotly, format_path_step, format_arc, format_rgba -from igraph.drawing.utils import Point, euclidean_distance, intersect_bezier_curve_and_circle +from igraph.drawing.plotly.utils import ( + find_plotly, + format_path_step, + format_arc, + format_rgba, +) +from igraph.drawing.utils import ( + Point, + euclidean_distance, + intersect_bezier_curve_and_circle, +) __all__ = ("PlotlyEdgeDrawer",) @@ -135,17 +144,15 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): # Draw the curve from the first vertex to the midpoint of the base # of the arrow head - path.append(format_path_step( - "C", [aux1, aux2, [x_arrow_mid, y_arrow_mid]] - )) + path.append(format_path_step("C", [aux1, aux2, [x_arrow_mid, y_arrow_mid]])) else: # FIXME: this is tricky in plotly, let's skip for now ## Determine where the edge intersects the circumference of the ## vertex shape. - #x2, y2 = dest_vertex.shape.intersection_point( + # x2, y2 = dest_vertex.shape.intersection_point( # x2, y2, x1, y1, dest_vertex.size - #) + # ) # Draw the arrowhead angle = atan2(y_dest - y2, x_dest - x2) @@ -167,15 +174,18 @@ def draw_directed_edge(self, edge, src_vertex, dest_vertex): aux_points[0][1] + aux_points[1][1] ) / 2.0 # Draw the line - path.append(format_path_step( - "L", Point(x_arrow_mid, y_arrow_mid), - )) + path.append( + format_path_step( + "L", + Point(x_arrow_mid, y_arrow_mid), + ) + ) - path = ' '.join(path) + path = " ".join(path) # Draw the edge stroke = dict( - type='path', + type="path", path=path, line_color=format_rgba(edge.color), line_width=edge.width, @@ -206,7 +216,7 @@ def draw_loop_edge(self, edge, vertex): center_x = vertex.position[0] + cos(pi / 4) * radius / 2.0 center_y = vertex.position[1] - sin(pi / 4) * radius / 2.0 stroke = dict( - type='path', + type="path", path=format_arc( (center_x, center_y), radius / 2.0, @@ -237,9 +247,7 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): fig = self.context - path = [ - format_path_step("M", src_vertex.position) - ] + path = [format_path_step("M", src_vertex.position)] if edge.curved: (x1, y1), (x2, y2) = src_vertex.position, dest_vertex.position @@ -250,19 +258,25 @@ def draw_undirected_edge(self, edge, src_vertex, dest_vertex): y1 + 2 * y2 ) / 3.0 + edge.curved * 0.5 * (x2 - x1) - path.append(format_path_step( - "C", [aux1, aux2, dest_vertex.position], - )) + path.append( + format_path_step( + "C", + [aux1, aux2, dest_vertex.position], + ) + ) else: - path.append(format_path_step( - "L", dest_vertex.position, - )) + path.append( + format_path_step( + "L", + dest_vertex.position, + ) + ) - path = ' '.join(path) + path = " ".join(path) stroke = dict( - type='path', + type="path", path=path, line_color=format_rgba(edge.color), line_width=edge.width, diff --git a/src/igraph/drawing/plotly/graph.py b/src/igraph/drawing/plotly/graph.py index e45e7d3db..e24cc1102 100644 --- a/src/igraph/drawing/plotly/graph.py +++ b/src/igraph/drawing/plotly/graph.py @@ -140,7 +140,7 @@ def draw(self, graph, *args, **kwds): hull = [group[i] for i in convex_hull([layout[idx] for idx in group])] # Calculate the preferred rounding radius for the corners - #FIXME + # FIXME corner_radius = 1.25 * max(vertex_builder[idx].size for idx in hull) # Construct the polygon @@ -177,7 +177,8 @@ def draw(self, graph, *args, **kwds): fig.add_trace( plotly.graph_objects.Bar( name=str(color_id), - x=[], y=[], + x=[], + y=[], fillcolor=facecolor, line_color=color, ) @@ -246,10 +247,10 @@ def draw(self, graph, *args, **kwds): (labels[i], edge_builder[i], graph.es[i]) for i in range(graph.ecount()) ) lab_args = { - 'text': [], - 'x': [], - 'y': [], - 'color': [], + "text": [], + "x": [], + "y": [], + "color": [], # FIXME: horizontal/vertical alignment, offset, etc? } for label, visual_edge, edge in edge_label_iter: @@ -264,18 +265,18 @@ def draw(self, graph, *args, **kwds): if label is None: continue - lab_args['text'].append(label) - lab_args['x'].append(x) - lab_args['y'].append(y) - lab_args['color'].append(visual_edge.label_color) + lab_args["text"].append(label) + lab_args["x"].append(x) + lab_args["y"].append(y) + lab_args["color"].append(visual_edge.label_color) stroke = plotly.graph_objects.Scatter( - mode='text', + mode="text", **lab_args, ) fig.add_trace(stroke) # Despine fig.update_layout( - yaxis={'visible': False, 'showticklabels': False}, - xaxis={'visible': False, 'showticklabels': False}, + yaxis={"visible": False, "showticklabels": False}, + xaxis={"visible": False, "showticklabels": False}, ) diff --git a/src/igraph/drawing/plotly/polygon.py b/src/igraph/drawing/plotly/polygon.py index c0978e42b..49f96ce87 100644 --- a/src/igraph/drawing/plotly/polygon.py +++ b/src/igraph/drawing/plotly/polygon.py @@ -46,11 +46,11 @@ def draw(self, points, corner_radius=0, **kwds): # We need to repeat the initial point to get a closed shape x = [p[0] for p in points] + [points[0][0]] y = [p[1] for p in points] + [points[0][1]] - kwds['mode'] = kwds.get('mode', 'line') + kwds["mode"] = kwds.get("mode", "line") stroke = plotly.graph_objects.Scatter( - x=x, - y=y, - **kwds, + x=x, + y=y, + **kwds, ) fig.add_trace(stroke) @@ -67,10 +67,12 @@ def draw(self, points, corner_radius=0, **kwds): # Okay, move to the last corner, adjusted by corner_radii[-1] # towards the first corner path = [] - path.append(format_path_step( - "M", - [points[-1].towards(points[0], corner_radii[-1])], - )) + path.append( + format_path_step( + "M", + [points[-1].towards(points[0], corner_radii[-1])], + ) + ) # Now, for each point in points, draw a line towards the # corner, stopping before it in a distance of corner_radii[idx], @@ -78,22 +80,26 @@ def draw(self, points, corner_radius=0, **kwds): u = points[-1] for idx, (v, w) in enumerate(consecutive_pairs(points, True)): radius = corner_radii[idx] - path.append(format_path_step( - "L", - [v.towards(u, radius)], - )) + path.append( + format_path_step( + "L", + [v.towards(u, radius)], + ) + ) aux1 = v.towards(u, radius / 2) aux2 = v.towards(w, radius / 2) - path.append(format_path_step( - "C", - [aux1, aux2, v.towards(w, corner_radii[idx])], - )) + path.append( + format_path_step( + "C", + [aux1, aux2, v.towards(w, corner_radii[idx])], + ) + ) u = v # Close path - path = "".join(path).strip(" ")+" Z" + path = "".join(path).strip(" ") + " Z" stroke = dict( type="path", path=path, diff --git a/src/igraph/drawing/plotly/utils.py b/src/igraph/drawing/plotly/utils.py index be28dfc50..7402d5c5f 100644 --- a/src/igraph/drawing/plotly/utils.py +++ b/src/igraph/drawing/plotly/utils.py @@ -1,7 +1,7 @@ from igraph.drawing.utils import FakeModule, Point from typing import Any -__all__ = ("find_plotly", ) +__all__ = ("find_plotly",) def find_plotly() -> Any: @@ -40,14 +40,14 @@ def format_arc(center, radius_x, radius_y, theta1, theta2, N=100, closed=False): xc, yc = center dt = 1.0 * (theta2 - theta1) - t = [theta1 + dt * i / (N-1) for i in range(N)] + t = [theta1 + dt * i / (N - 1) for i in range(N)] x = [xc + radius_x * math.cos(i) for i in t] y = [yc + radius_y * math.sin(i) for i in t] - path = f'M {x[0]}, {y[0]}' + path = f"M {x[0]}, {y[0]}" for k in range(1, len(t)): - path += f'L{x[k]}, {y[k]}' + path += f"L{x[k]}, {y[k]}" if closed: - path += ' Z' + path += " Z" return path @@ -58,7 +58,7 @@ def format_rgba(color): if isinstance(color, float): if color > 1: - color /= 255. + color /= 255.0 color = [color] * 3 r = int(255 * color[0]) @@ -69,6 +69,5 @@ def format_rgba(color): else: a = 255 - colstr = f'rgba({r},{g},{b},{a})' + colstr = f"rgba({r},{g},{b},{a})" return colstr - diff --git a/src/igraph/drawing/plotly/vertex.py b/src/igraph/drawing/plotly/vertex.py index 4d6f6df52..fe39c3a56 100644 --- a/src/igraph/drawing/plotly/vertex.py +++ b/src/igraph/drawing/plotly/vertex.py @@ -6,7 +6,7 @@ from igraph.drawing.metamagic import AttributeCollectorBase from .utils import find_plotly, format_rgba -__all__ = ('PlotlyVerticesDrawer',) +__all__ = ("PlotlyVerticesDrawer",) plotly = find_plotly() @@ -43,7 +43,6 @@ class VisualVertexBuilder(AttributeCollectorBase): return VisualVertexBuilder - def draw(self, visual_vertex, vertex, point): if visual_vertex.size <= 0: return @@ -51,23 +50,23 @@ def draw(self, visual_vertex, vertex, point): fig = self.fig marker_kwds = {} - marker_kwds['x'] = [point[0]] - marker_kwds['y'] = [point[1]] - marker_kwds['marker'] = { - 'symbol': visual_vertex.shape, - 'size': visual_vertex.size, - 'color': format_rgba(visual_vertex.color), - 'line_color': format_rgba(visual_vertex.frame_color), + marker_kwds["x"] = [point[0]] + marker_kwds["y"] = [point[1]] + marker_kwds["marker"] = { + "symbol": visual_vertex.shape, + "size": visual_vertex.size, + "color": format_rgba(visual_vertex.color), + "line_color": format_rgba(visual_vertex.frame_color), } - #if visual_vertex.label is not None: + # if visual_vertex.label is not None: # text_kwds['x'].append(point[0]) # text_kwds['y'].append(point[1]) # text_kwds['text'].append(visual_vertex.label) # Draw dots stroke = plotly.graph_objects.Scatter( - mode='markers', + mode="markers", **marker_kwds, ) fig.add_trace(stroke) @@ -79,17 +78,15 @@ def draw_label(self, visual_vertex, point, **kwds): fig = self.fig text_kwds = {} - text_kwds['x'] = [point[0]] - text_kwds['y'] = [point[1]] - text_kwds['text'].append(visual_vertex.label) + text_kwds["x"] = [point[0]] + text_kwds["y"] = [point[1]] + text_kwds["text"].append(visual_vertex.label) # TODO: add more options # Draw text labels stroke = plotly.graph_objects.Scatter( - mode='text', + mode="text", **text_kwds, ) fig.add_trace(stroke) - - diff --git a/src/igraph/drawing/text.py b/src/igraph/drawing/text.py index df48bbdb9..57d2f9a90 100644 --- a/src/igraph/drawing/text.py +++ b/src/igraph/drawing/text.py @@ -4,7 +4,7 @@ from enum import Enum -__all__ = ("TextAlignment", ) +__all__ = ("TextAlignment",) ##################################################################### From a31499e62cd8cca27a4bcbf284e003835afdbf28 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 28 Oct 2021 21:52:12 +0200 Subject: [PATCH 0484/1681] chore: preparing for the rename of python-igraph to igraph --- README.md | 34 +++++++++--------- doc/Makefile | 8 ++--- doc/source/conf.py | 12 +++---- doc/source/include/global.rst | 1 - doc/source/index.rst | 6 ++-- doc/source/install.rst | 66 ++++++++++++++++++---------------- docker/jupyter/Dockerfile | 5 +-- docker/minimal/Dockerfile | 2 +- scripts/mkdoc.sh | 8 +++-- setup.py | 19 +++++----- src/igraph/__init__.py | 15 ++++---- src/igraph/configuration.py | 2 +- src/igraph/drawing/__init__.py | 8 ++--- src/igraph/drawing/graph.py | 5 ++- 14 files changed, 94 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 7c10bd183..f097cd460 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build and test with tox](https://github.com/igraph/python-igraph/actions/workflows/build.yml/badge.svg)](https://github.com/igraph/python-igraph/actions/workflows/build.yml) -[![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue)](https://pypi.python.org/pypi/python-igraph) -[![PyPI wheels](https://img.shields.io/pypi/wheel/python-igraph.svg)](https://pypi.python.org/pypi/python-igraph) +[![PyPI pyversions](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10-blue)](https://pypi.python.org/pypi/igraph) +[![PyPI wheels](https://img.shields.io/pypi/wheel/igraph.svg)](https://pypi.python.org/pypi/igraph) Python interface for the igraph library --------------------------------------- @@ -13,7 +13,7 @@ analysis of large graphs. This repository contains the source code to the Python interface of igraph. -You can learn more about python-igraph [on our website](http://igraph.org/python/). +You can learn more about igraph [on our website](http://igraph.org/python/). ## Installation from PyPI @@ -23,14 +23,14 @@ Therefore, running the following command should work without having to compile anything during installation: ``` -pip install python-igraph +pip install igraph ``` See details in [Installing Python Modules](https://docs.python.org/3/installing/). ### Installation from source with pip on Debian / Ubuntu and derivatives -If you need to compile python-igraph from source for some reason, you need to +If you need to compile igraph from source for some reason, you need to install some dependencies first: ``` @@ -40,7 +40,7 @@ sudo apt install build-essential python-dev libxml2 libxml2-dev zlib1g-dev and then run ``` -pip install python-igraph +pip install igraph ``` This should compile the C core of igraph as well as the Python extension @@ -48,12 +48,12 @@ automatically. ### Installation from source on Windows -It is now also possible to compile `python-igraph` from source under Windows for +It is now also possible to compile `igraph` from source under Windows for Python 3.6 and later. Make sure that you have Microsoft Visual Studio 2015 or later installed, and of course Python 3.6 or later. First extract the source to a suitable directory. If you launch the Developer command prompt and navigate to the directory where you extracted the source code, you should be able to build -and install python-igraph using `python setup.py install` +and install igraph using `python setup.py install` You may need to set the architecture that you are building on explicitly by setting the environment variable @@ -85,8 +85,8 @@ This mentions that > CMake projects should use: `-DCMAKE_TOOLCHAIN_FILE=[vcpkg build script]` -which we will do next. In order to build `python-igraph` correctly, you also -need to set some other environment variables before building `python-igraph`: +which we will do next. In order to build `igraph` correctly, you also +need to set some other environment variables before building `igraph`: ``` set IGRAPH_CMAKE_EXTRA_ARGS=-DVCPKG_TARGET_TRIPLET=x64-windows-static-md -DCMAKE_TOOLCHAIN_FILE=[vcpkg build script] @@ -96,7 +96,7 @@ set IGRAPH_EXTRA_LIBRARIES=libxml2,lzma,zlib,iconv,charset set IGRAPH_EXTRA_DYNAMIC_LIBRARIES: wsock32,ws2_32 ``` -You can now build and install `python-igraph` again by simply running `python +You can now build and install `igraph` again by simply running `python setup.py build`. Please make sure to use a clean source tree, if you built previously without GraphML, it will not update the build. @@ -120,7 +120,7 @@ error message, you are probably okay. You can then proceed with the installation using pip: ```bash -pip install python-igraph --install-option="--use-pkg-config" +pip install igraph --install-option="--use-pkg-config" ``` Alternatively, if you have already downloaded and extracted the source code @@ -179,13 +179,13 @@ python -m unittest ## Contributing -Contributions to `python-igraph` are welcome! +Contributions to `igraph` are welcome! If you want to add a feature, fix a bug, or suggest an improvement, open an issue on this repository and we'll try to answer. If you have a piece of code that you would like to see included in the main tree, open a PR on this repo. -To start developing `python-igraph`, follow the steps below (these are +To start developing `igraph`, follow the steps below (these are for Linux, Windows users should change the system commands a little). First, clone this repo (e.g. via https) and enter the folder: @@ -224,17 +224,15 @@ We aim to keep up with the development cycle of Python and support all official Python versions that have not reached their end of life yet. Currently this means that we support Python 3.6 to 3.9, inclusive. Please refer to [this page](https://devguide.python.org/#branchstatus) for the status of Python -branches and let us know if you encounter problems with `python-igraph` on any +branches and let us know if you encounter problems with `igraph` on any of the non-EOL Python versions. Continuous integration tests are regularly executed on all non-EOL Python branches. -Python 2 support has been dropped with the release of `python-igraph` 0.9. - ### PyPy -This version of python-igraph is compatible with [PyPy](http://pypy.org/) and +This version of igraph is compatible with [PyPy](http://pypy.org/) and is regularly tested on [PyPy](http://pypy.org/) with ``tox``. However, the PyPy version falls behind the CPython version in terms of performance; for instance, running all the tests takes ~5 seconds on my machine with CPython and diff --git a/doc/Makefile b/doc/Makefile index 2d2eb292b..aa1a6dbf8 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -72,17 +72,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-igraph.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/igraph.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-igraph.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/igraph.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/python-igraph" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-igraph" + @echo "# mkdir -p $$HOME/.local/share/devhelp/igraph" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/igraph" @echo "# devhelp" epub: diff --git a/doc/source/conf.py b/doc/source/conf.py index e94dd981f..f2c503a29 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# python-igraph documentation build configuration file, created by +# igraph documentation build configuration file, created by # sphinx-quickstart on Thu Jun 17 11:36:14 2010. # # This file is execfile()d with the current directory set to its containing dir. @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. -project = u'python-igraph' +project = u'igraph' copyright = u'2010-{0}, The igraph development team'.format(datetime.now().year) # The version info for the project you're documenting, acts as replacement for @@ -170,7 +170,7 @@ #html_file_suffix = '' # Output file base name for HTML help builder. -htmlhelp_basename = 'python-igraphdoc' +htmlhelp_basename = 'igraphdoc' # -- Options for LaTeX output -------------------------------------------------- @@ -184,7 +184,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'python-igraph.tex', u'python-igraph Documentation', + ('index', 'igraph.tex', u'igraph Documentation', u'The igraph development team', 'manual'), ] @@ -217,7 +217,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'python-igraph', u'python-igraph Documentation', + ('index', 'igraph', u'igraph Documentation', [u'The igraph development team'], 1) ] @@ -225,7 +225,7 @@ # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'python-igraph' +epub_title = u'igraph' epub_author = u'The igraph development team' epub_publisher = u'The igraph development team' epub_copyright = u'2010, The igraph development team' diff --git a/doc/source/include/global.rst b/doc/source/include/global.rst index 7a928c465..d9462c53a 100644 --- a/doc/source/include/global.rst +++ b/doc/source/include/global.rst @@ -1,3 +1,2 @@ .. |igraph| replace:: *igraph* -.. |python-igraph| replace:: *python-igraph* diff --git a/doc/source/index.rst b/doc/source/index.rst index cb4afaa66..3203cf801 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,11 +1,11 @@ -.. python-igraph documentation master file, created by sphinx-quickstart on Thu Dec 11 16:02:35 2008. +.. igraph documentation master file, created by sphinx-quickstart on Thu Dec 11 16:02:35 2008. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. .. include:: include/global.rst -Welcome to python-igraph's documentation! -========================================= +Welcome to the documentation of the Python interface of igraph +============================================================== Contents: diff --git a/doc/source/install.rst b/doc/source/install.rst index f786c12d6..665339bf7 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -24,7 +24,7 @@ are often called *binary packages*, while the raw source code is usually referre In general, you should almost always opt for the binary package unless a binary package is not available for your platform or you have some local modifications that you want to incorporate into |igraph|'s source. `Installation from a binary package`_ tells you how to -install |igraph| from a precompiled binary package on various platforms. `Compiling python-igraph +install |igraph| from a precompiled binary package on various platforms. `Compiling igraph from source`_ tells you how to compile |igraph| from the source package. Installation from a binary package @@ -35,13 +35,13 @@ Installing |igraph| from the Python Package Index To ensure getting the latest binary release of |igraph|'s Python interface, it is recommended that you install it from the `Python Package Index -`_ (PyPI), which has installers for Windows, Linux, +`_ (PyPI), which has installers for Windows, Linux, and macOS. We aim to provide binary packages for the three latest minor versions of Python 3.x. -To install |python-igraph| globally, use the following command (you probably need -administrator/root privileges):: +To install the Python interface of |igraph| globally, use the following command +(you probably need administrator/root privileges):: - $ pip install python-igraph + $ pip install igraph Many users like to install packages into a project-specific `virtual environment `_. @@ -49,13 +49,13 @@ A variation of the following commands should work on most platforms:: $ python -m venv venv $ source venv/bin/activate - $ pip install python-igraph + $ pip install igraph Alternatively, if you do not want to activate the virtualenv, you can call the ``pip`` executable in it directly:: $ python -m venv venv - $ venv/bin/pip install python-igraph + $ venv/bin/pip install igraph Installing |igraph| via Conda ----------------------------- @@ -70,7 +70,7 @@ using conda. That can be achieved by running the following command:: ------------------- Precompiled Windows wheels for |igraph|'s Python interface are available on the `Python Package Index -`_ (see `Installing igraph from the Python Package +`_ (see `Installing igraph from the Python Package Index`_). Graph plotting in |igraph| is implemented using a third-party package called `Cairo @@ -95,7 +95,7 @@ If PyCairo was successfully installed, this will display a Petersen graph. ----------------- Precompiled macOS wheels for |igraph|'s Python interface are available on the `Python Package Index -`_ (see `Installing igraph from the Python Package +`_ (see `Installing igraph from the Python Package Index`_). Graph plotting in |igraph| is implemented using a third-party package called `Cairo @@ -120,44 +120,47 @@ If Cairo was successfully installed, this will display a Petersen graph. |igraph|'s Python interface and its dependencies have been packaged for most popular Linux distributions, including Arch Linux, Debian, Fedora, GNU Guix, NixOS, and Ubuntu. -|python-igraph| and its underlying |igraph| C core are usually in two different packages, but +|igraph| and its underlying C core are usually in two different packages, but your package manager should take care of that dependency for you. .. note:: Distribution packages can be outdated: if you find that's the case for you, you may - choose to install |igraph| from the `Python Package Index `_ + choose to install |igraph| from the `Python Package Index `_ instead to get a more recent release (see `Installing igraph from the Python Package Index`_). -Compiling |python-igraph| from source +Compiling |igraph| from source ===================================== -|python-igraph| binds itself into the main |igraph| library using some glue code written in C, so you'll need -both a C compiler and the library itself. Source tarballs of |python-igraph| obtained from PyPI already -contain a matching version of the C core of |igraph|. +|igraph| binds itself into the main |igraph| library using some glue code +written in C, so you'll need both a C compiler and the library itself. Source +tarballs of |igraph| obtained from PyPI already contain a matching version of +the C core of |igraph|. -There are two common scenarios to compile |python-igraph| from source: +There are two common scenarios to compile |igraph| from source: -1. Your would like to use the latest development version from Github, usually to try out some recently - added features +1. Your would like to use the latest development version from Github, usually + to try out some recently added features -2. The PyPI repository does not include precompiled binaries for your system. This can happen if your operating - system is not covered by our continuous development. +2. The PyPI repository does not include precompiled binaries for your system. + This can happen if your operating system is not covered by our continuous + development. Both will be covered in the next sections. Compiling using pip ------------------- -If you want the development version of |python-igraph|, call:: +If you want the development version of |igraph|, call:: $ pip install git+https://github.com/igraph/python-igraph -``pip`` is smart enough to download the sources from Github, initialize the submodule for the |igraph| C core, -compile it, and then compile |python-igraph| against it and install it. As above, a virtual environment is +``pip`` is smart enough to download the sources from Github, initialize the +submodule for the |igraph| C core, compile it, and then compile the Python +interface against it and install it. As above, a virtual environment is a commonly used sandbox to test experimental packages. If you want the latest release from PyPI but prefer to (or have to) install from source, call:: - $ pip install --no-binary ':all:' python-igraph + $ pip install --no-binary ':all:' igraph .. note:: If there is no binary for your system anyway, you can just try without the ``--no-binary`` option and obtain the same result. @@ -165,8 +168,8 @@ If you want the latest release from PyPI but prefer to (or have to) install from Compiling step by step ---------------------- -This section should be rarely used in practice but explains how to compile and install |python-igraph| step -by step without ``pip``. +This section should be rarely used in practice but explains how to compile and +install |igraph| step by step without ``pip``. First, obtain the bleeding-edge source code from Github:: @@ -186,18 +189,19 @@ Third, if you cloned the source from Github, initialize the ``git`` submodule fo $ git submodule update --init -.. note:: If you prefer to compile and link |python-igraph| against an existing |igraph| C core, for instance +.. note:: If you prefer to compile and link |igraph| against an existing |igraph| C core, for instance the one you installed with your package manager, you can skip the ``git`` submodule initialization step. If you downloaded a tarball, you also need to remove the ``vendor/source/igraph`` folder because the setup script - will look for the vendored |igraph| copy first. However, a particular version of |python-igraph| is guaranteed - to work only with the version of the C core that is bundled with it (or with the revision that the ``git`` + will look for the vendored |igraph| copy first. However, a particular + version of the Python interface is guaranteed to work only with the version + of the C core that is bundled with it (or with the revision that the ``git`` submodule points to). Fourth, call the standard Python ``setup.py`` script, e.g. for compiling:: $ python setup.py build -(press Enter when prompted). That will compile the |python-igraph| package in a subfolder called +(press Enter when prompted). That will compile the Python interface in a subfolder called ``build/lib.``, e.g. `build/lib.linux-x86_64-3.8`. You can add that folder to your ``PYTHONPATH`` if you want to import directly from it, or you can call the ``setup.py`` script to install it from there:: @@ -209,7 +213,7 @@ script to install it from there:: Testing your installation ------------------------- -The unit tests of ``python-igraph`` are implemented with the standard ``unittest`` module so you can +The unit tests are implemented with the standard ``unittest`` module so you can run them like this from your the source folder:: $ python -m unittest discover diff --git a/docker/jupyter/Dockerfile b/docker/jupyter/Dockerfile index 46609ae8f..e80376cf9 100644 --- a/docker/jupyter/Dockerfile +++ b/docker/jupyter/Dockerfile @@ -2,7 +2,4 @@ FROM jupyter/notebook MAINTAINER Tamas Nepusz LABEL Description="Docker image that contains Jupyter with a pre-compiled version of igraph's Python interface" RUN apt-get -y update && apt-get -y install build-essential libxml2-dev zlib1g-dev python-dev python-pip pkg-config libffi-dev libcairo-dev -RUN pip2 install python-igraph -RUN pip2 install cairocffi -RUN pip3 install python-igraph -RUN pip3 install cairocffi \ No newline at end of file +RUN pip3 install igraph cairocffi matplotlib \ No newline at end of file diff --git a/docker/minimal/Dockerfile b/docker/minimal/Dockerfile index d389de1d5..dba6d1442 100644 --- a/docker/minimal/Dockerfile +++ b/docker/minimal/Dockerfile @@ -2,7 +2,7 @@ FROM python:latest MAINTAINER Tamas Nepusz LABEL Description="Simple Docker image that contains a pre-compiled version of igraph's Python interface" -RUN pip3 install python-igraph cairocffi +RUN pip3 install igraph cairocffi CMD /usr/local/bin/igraph diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index b49ffc3aa..f5ac87264 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -29,19 +29,21 @@ PWD=`pwd` echo "Removing existing documentation..." rm -rf "${DOC_API_FOLDER}/html" "${DOC_API_FOLDER}/pdf" -echo "Removing existing python-igraph eggs from virtualenv..." +echo "Removing existing igraph and python-igraph eggs from virtualenv..." SITE_PACKAGES_DIR=`.venv/bin/python3 -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])'` +rm -rf "${SITE_PACKAGES_DIR}"/igraph*.egg +rm -rf "${SITE_PACKAGES_DIR}"/igraph*.egg-link rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg-link -echo "Installing python-igraph in virtualenv..." +echo "Installing igraph in virtualenv..." rm -f dist/*.whl && .venv/bin/python setup.py bdist_wheel && .venv/bin/pip install --force-reinstall dist/*.whl IGRAPHDIR=`.venv/bin/python3 -c 'import igraph, os; print(os.path.dirname(igraph.__file__))'` echo "Generating HTML documentation..." "$PYDOCTOR" \ - --project-name "python-igraph" \ + --project-name "igraph" \ --project-url "https://igraph.org/python" \ --introspect-c-modules \ --make-html \ diff --git a/setup.py b/setup.py index c4944ee0b..5f15a6520 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,6 @@ import shlex import shutil import subprocess -import sys import sysconfig from contextlib import contextmanager @@ -517,7 +516,7 @@ def run(self): version_header = igraph_build_dir / "include" / "igraph_version.h" if not version_header.exists(): raise RuntimeError( - "You need to build the C core of igraph first before generating a source tarball of python-igraph" + "You need to build the C core of igraph first before generating a source tarball of the Python interface of igraph" ) with version_header.open("r") as fp: @@ -535,8 +534,8 @@ def run(self): ) if not is_git_repo(igraph_source_repo): - # python-igraph was extracted from an official tarball so - # there is no need to tweak anything + # The Python interface was extracted from an official + # tarball so there is no need to tweak anything return sdist.run(self) else: # Clean up vendor/source/igraph with git @@ -552,7 +551,7 @@ def run(self): else: raise RuntimeError( "You need to build the C core of igraph first before " - "generating a source tarball of python-igraph" + "generating a source tarball of the Python interface" ) # Add a version file to the tarball @@ -781,7 +780,7 @@ def use_educated_guess(self) -> None: # Import version number from version.py so we only need to change it in # one place when a new release is created -__version__ = None +__version__: str = "" exec(open("src/igraph/version.py").read()) # Process command line options @@ -800,15 +799,15 @@ def use_educated_guess(self) -> None: sure you install the Python bindings of Cairo if you want to generate publication-quality graph plots. You can try either `pycairo `_ or `cairocffi `_, -``cairocffi`` is recommended, in particular if you are on Python 3.x because -there were bug reports affecting igraph graph plots in Jupyter notebooks -when using ``pycairo`` (but not with ``cairocffi``). +``cairocffi`` is recommended because there were bug reports affecting igraph +graph plots in Jupyter notebooks when using ``pycairo`` (but not with +``cairocffi``). """ headers = ["src/_igraph/igraphmodule_api.h"] if not SKIP_HEADER_INSTALL else [] options = dict( - name="python-igraph", + name="igraph", version=__version__, url="https://igraph.org/python", description="High performance graph data structures and algorithms", diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 1e9178902..96f7de493 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -1960,9 +1960,8 @@ def to_graph_tool( Note: because of the restricted data types in graph-tool, vertex and edge attributes require to be type-consistent across all vertices or edges. If you set the property for only some vertices/edges, the other - will be tagged as None in python-igraph, so they can only be converted - to graph-tool with the type 'object' and any other conversion will - fail. + will be tagged as None in igraph, so they can only be converted to + graph-tool with the type 'object' and any other conversion will fail. @param graph_attributes: dictionary of graph attributes to transfer. Keys are attributes from the graph, values are data types (see @@ -5243,21 +5242,21 @@ def get_include(): import igraph paths = [ - # The following path works if python-igraph is installed already + # The following path works if igraph is installed already os.path.join( sys.prefix, "include", "python{0}.{1}".format(*sys.version_info), - "python-igraph", + "igraph", ), - # Fallback for cases when python-igraph is not installed but + # Fallback for cases when igraph is not installed but # imported directly from the source tree - os.path.join(os.path.dirname(igraph.__file__), "..", "src"), + os.path.join(os.path.dirname(igraph.__file__), "..", "src", "_igraph"), ] for path in paths: if os.path.exists(os.path.join(path, "igraphmodule_api.h")): return os.path.abspath(path) - raise ValueError("cannot find the header files of python-igraph") + raise ValueError("cannot find the header files of the Python interface of igraph") def read(filename, *args, **kwds): diff --git a/src/igraph/configuration.py b/src/igraph/configuration.py index 43d890219..e81a1dfa4 100644 --- a/src/igraph/configuration.py +++ b/src/igraph/configuration.py @@ -22,7 +22,7 @@ def get_platform_image_viewer(): """Returns the path of an image viewer on the given platform. - Deprecated since python-igraph 0.9.1 and will be removed in 0.10.0. + Deprecated since igraph 0.9.1 and will be removed in 0.10.0. @deprecated: This function was only used by the now-deprecated C{show()} method of the Plot class. diff --git a/src/igraph/drawing/__init__.py b/src/igraph/drawing/__init__.py index 244bb9116..288b66468 100644 --- a/src/igraph/drawing/__init__.py +++ b/src/igraph/drawing/__init__.py @@ -312,13 +312,13 @@ def save(self, fname=None): def show(self): """Saves the plot to a temporary file and shows it. - This method is deprecated from python-igraph 0.9.1 and will be removed in + This method is deprecated from igraph 0.9.1 and will be removed in version 0.10.0. @deprecated: Opening an image viewer with a temporary file never worked reliably across platforms. """ - warn("Plot.show() is deprecated from python-igraph 0.9.1", DeprecationWarning) + warn("Plot.show() is deprecated from igraph 0.9.1", DeprecationWarning) if not isinstance(self._surface, cairo.ImageSurface): sur = cairo.ImageSurface( @@ -426,8 +426,8 @@ def plot(obj, target=None, bbox=(0, 0, 600, 600), *args, **kwds): - C{None} -- a temporary file will be created and the object will be plotted there. igraph will attempt to open an image viewer and show - the temporary file. This feature is deprecated from python-igraph - version 0.9.1 and will be removed in 0.10.0. + the temporary file. This feature is deprecated from igraph version + 0.9.1 and will be removed in 0.10.0. @param bbox: the bounding box of the plot. It must be a tuple with either two or four integers, or a L{BoundingBox} object. If this is a tuple diff --git a/src/igraph/drawing/graph.py b/src/igraph/drawing/graph.py index c47b19193..41a950110 100644 --- a/src/igraph/drawing/graph.py +++ b/src/igraph/drawing/graph.py @@ -536,8 +536,7 @@ class UbiGraphDrawer(AbstractXMLRPCDrawer, AbstractGraphDrawer): @deprecated: UbiGraph has not received updates since 2008 and is now not available for download (at least not from the official sources). - The UbiGraph graph drawer will be removed from python-igraph in - 0.10.0. + The UbiGraph graph drawer will be removed from igraph in 0.10.0. """ def __init__(self, url="http://localhost:20738/RPC2"): @@ -548,7 +547,7 @@ def __init__(self, url="http://localhost:20738/RPC2"): self.edge_defaults = dict(color="#ffffff", width=1.0) warn( - "UbiGraphDrawer is deprecated from python-igraph 0.9.4", + "UbiGraphDrawer is deprecated from igraph 0.9.4", DeprecationWarning ) From d17cd6cb29815da6ae640a3a4b92974d8f142fce Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 28 Oct 2021 22:03:39 +0200 Subject: [PATCH 0485/1681] ci: use pytest exclusively --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebd7e5fad..34fc3d24d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -206,8 +206,8 @@ jobs: - name: Test run: | - pip install numpy scipy pandas networkx - python -m unittest -vvv + pip install numpy scipy pandas networkx pytest + python -m pytest tests - uses: actions/upload-artifact@v2 with: From 47bbe4a4be0a7625ffb58a4be93b90680125de84 Mon Sep 17 00:00:00 2001 From: Tamas Nepusz Date: Thu, 28 Oct 2021 22:10:06 +0200 Subject: [PATCH 0486/1681] chore: bumped version to 0.9.8 --- CHANGELOG.md | 8 ++++++++ doc/source/conf.py | 4 ++-- src/igraph/version.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d6851f34..83715dd40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## [0.9.8] +### Changed + +- `python-igraph` is now simply `igraph` on PyPI. `python-igraph` will be + updated until Sep 1, 2022 but it will only be a stub package that pulls in + `igraph` as its only dependency, with a matching version number. Please + update your projects to depend on `igraph` instead of `python-igraph` to + keep on receiving updates after Sep 1, 2022. + ### Fixed - `setup.py` no longer uses `distutils`, thanks to diff --git a/doc/source/conf.py b/doc/source/conf.py index f2c503a29..4aa58a76e 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -51,9 +51,9 @@ # built documents. # # The short X.Y version. -version = '0.9.7' +version = '0.9.8' # The full version, including alpha/beta/rc tags. -release = '0.9.7' +release = '0.9.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/src/igraph/version.py b/src/igraph/version.py index 752360a77..543a50026 100644 --- a/src/igraph/version.py +++ b/src/igraph/version.py @@ -1,2 +1,2 @@ -__version_info__ = (0, 9, 7) +__version_info__ = (0, 9, 8) __version__ = ".".join("{0}".format(x) for x in __version_info__) From 0c5478c937c333db0801ca8691d28e9768f73f1b Mon Sep 17 00:00:00 2001 From: Markus Elfring Date: Tue, 2 Nov 2021 19:38:08 +0100 Subject: [PATCH 0487/1681] Issue #452: Convert five statements to the usage of augmented assignments Augmented assignment statements became available with Python 2. https://docs.python.org/3/whatsnew/2.0.html#augmented-assignment Thus improve five source code places accordingly. Signed-off-by: Markus Elfring --- src/igraph/__init__.py | 8 ++++---- src/igraph/layout.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 96f7de493..65f44c4ee 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -2649,8 +2649,8 @@ def write_svg( x2 = layout[vidxs[1]][0] y2 = layout[vidxs[1]][1] angle = math.atan2(y2 - y1, x2 - x1) - x2 = x2 - vertex_size * math.cos(angle) - y2 = y2 - vertex_size * math.sin(angle) + x2 -= vertex_size * math.cos(angle) + y2 -= vertex_size * math.sin(angle) print(" Date: Sat, 30 Oct 2021 10:55:31 +1100 Subject: [PATCH 0488/1681] Fix docstrings for pydoctor --- scripts/mkdoc.sh | 7 +- scripts/patch-pydoctor.sh | 23 +++ scripts/patch_modularized_graph_methods.py | 46 ++++++ scripts/pydoctor-21.2.2.patch | 167 +++++++++++++++++++++ 4 files changed, 242 insertions(+), 1 deletion(-) create mode 100755 scripts/patch-pydoctor.sh create mode 100644 scripts/patch_modularized_graph_methods.py create mode 100644 scripts/pydoctor-21.2.2.patch diff --git a/scripts/mkdoc.sh b/scripts/mkdoc.sh index f5ac87264..c4fa154ac 100755 --- a/scripts/mkdoc.sh +++ b/scripts/mkdoc.sh @@ -26,6 +26,9 @@ fi PWD=`pwd` +echo "Patch pydoctor until they fix it" +$SCRIPTS_FOLDER/patch-pydoctor.sh ${ROOT_FOLDER} ${SCRIPTS_FOLDER} + echo "Removing existing documentation..." rm -rf "${DOC_API_FOLDER}/html" "${DOC_API_FOLDER}/pdf" @@ -39,9 +42,11 @@ rm -rf "${SITE_PACKAGES_DIR}"/python_igraph*.egg-link echo "Installing igraph in virtualenv..." rm -f dist/*.whl && .venv/bin/python setup.py bdist_wheel && .venv/bin/pip install --force-reinstall dist/*.whl -IGRAPHDIR=`.venv/bin/python3 -c 'import igraph, os; print(os.path.dirname(igraph.__file__))'` +echo "Patching modularized Graph methods" +.venv/bin/python3 ${SCRIPTS_FOLDER}/patch_modularized_graph_methods.py echo "Generating HTML documentation..." +IGRAPHDIR=`.venv/bin/python3 -c 'import igraph, os; print(os.path.dirname(igraph.__file__))'` "$PYDOCTOR" \ --project-name "igraph" \ --project-url "https://igraph.org/python" \ diff --git a/scripts/patch-pydoctor.sh b/scripts/patch-pydoctor.sh new file mode 100755 index 000000000..d6ff5a4f7 --- /dev/null +++ b/scripts/patch-pydoctor.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Patches PyDoctor to make it suitable to build python-igraph's documentation +# until our patches get upstreamed + +if [ "x$1" = x ]; then + echo "Usage: $0 ROOT_FOLDER PATCH_FOLDER" + exit 1 +fi + +set -e + +ROOT_FOLDER=$1 +PATCH_FOLDER=$(realpath $2) +${ROOT_FOLDER}/.venv/bin/pip uninstall -y pydoctor +${ROOT_FOLDER}/.venv/bin/pip install pydoctor==21.2.2 +PYDOCTOR_DIR=`.venv/bin/python -c 'import os,pydoctor;print(os.path.dirname(pydoctor.__file__))'` + +cd "${PYDOCTOR_DIR}" +# patch is confirmed to work with pydoctor 21.2.2 +patch -r deleteme.rej -N -p2 <${PATCH_FOLDER}/pydoctor-21.2.2.patch 2>/dev/null +rm -f deleteme.rej + + diff --git a/scripts/patch_modularized_graph_methods.py b/scripts/patch_modularized_graph_methods.py new file mode 100644 index 000000000..1521f8fa2 --- /dev/null +++ b/scripts/patch_modularized_graph_methods.py @@ -0,0 +1,46 @@ +import os +import sys +import inspect + +import igraph + + +# Get instance and classmethods +g = igraph.Graph() +methods = inspect.getmembers(g, predicate=inspect.ismethod) + +# Get the source code for each method and replace the method name +# in the signature +methodsources = {} +for mname, method in methods: + source = inspect.getsourcelines(method)[0] + fname = source[0][source[0].find('def ') + 4: source[0].find('(')] + newsource = [source[0].replace(fname, mname)] + source[1:] + methodsources[mname] = newsource + +# Make .new file with replacements +newmodule = igraph.__file__ + '.new' +with open(newmodule, 'wt') as fout: + with open(igraph.__file__, 'rt') as f: + for line in f: + for mname in methodsources: + # Catch the methods, excluding factories (e.g. 3d layouts) + if (mname + ' = _' in line) and ('(' not in line): + break + else: + fout.write(line) + continue + + # Method found, substitute + fout.write('\n') + for mline in methodsources[mname]: + # Correct indentation + fout.write(' ' + mline) + +# Move the new file back +with open(igraph.__file__, 'wt') as fout: + with open(newmodule, 'rt') as f: + fout.write(f.read()) + +# Delete .new file +os.remove(newmodule) diff --git a/scripts/pydoctor-21.2.2.patch b/scripts/pydoctor-21.2.2.patch new file mode 100644 index 000000000..15a23623f --- /dev/null +++ b/scripts/pydoctor-21.2.2.patch @@ -0,0 +1,167 @@ +diff --git a/pydoctor/astbuilder.py b/pydoctor/astbuilder.py +index 6b274c9..393ddab 100644 +--- a/pydoctor/astbuilder.py ++++ b/pydoctor/astbuilder.py +@@ -188,7 +188,7 @@ class ModuleVistor(ast.NodeVisitor): + self.newAttr = None + + def visit_Module(self, node: ast.Module) -> None: +- assert self.module.docstring is None ++ # assert self.module.docstring is None + + self.builder.push(self.module, 0) + if len(node.body) > 0 and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str): +diff --git a/pydoctor/epydoc2stan.py b/pydoctor/epydoc2stan.py +index 1b418ae..841c773 100644 +--- a/pydoctor/epydoc2stan.py ++++ b/pydoctor/epydoc2stan.py +@@ -191,6 +191,11 @@ class _EpydocLinker(DocstringLinker): + target = src.resolveName(identifier) + if target is not None: + return target ++ if isinstance(src, model.Class): ++ for base in src.allbases(): ++ target = base.resolveName(identifier) ++ if target is not None: ++ return target + src = src.parent + + # Walk up the object tree again and see if 'identifier' refers to an +@@ -528,7 +533,7 @@ class FieldHandler: + + def handleUnknownField(self, field: Field) -> None: + name = field.tag +- field.report(f"Unknown field '{name}'" ) ++ # field.report(f"Unknown field '{name}'" ) + self.unknowns[name].append(FieldDesc(name=field.arg, body=field.format())) + + def handle(self, field: Field) -> None: +diff --git a/pydoctor/model.py b/pydoctor/model.py +index d233639..a14a4df 100644 +--- a/pydoctor/model.py ++++ b/pydoctor/model.py +@@ -14,7 +14,7 @@ import platform + import sys + import types + from enum import Enum +-from inspect import Signature ++from inspect import signature, Signature + from optparse import Values + from pathlib import Path + from typing import ( +@@ -514,7 +514,8 @@ class Function(Inheritable): + is_async: bool + annotations: Mapping[str, Optional[ast.expr]] + decorators: Optional[Sequence[ast.expr]] +- signature: Signature ++ signature: Optional[Signature] ++ text_signature: str = "" + + def setup(self) -> None: + super().setup() +@@ -776,15 +777,24 @@ class System: + + def _introspectThing(self, thing: object, parent: Documentable, parentMod: _ModuleT) -> None: + for k, v in thing.__dict__.items(): +- if (isinstance(v, (types.BuiltinFunctionType, types.FunctionType)) ++ # TODO(ntamas): MethodDescriptorType and ClassMethodDescriptorType are Python 3.7 only. ++ if (isinstance(v, (types.BuiltinFunctionType, types.FunctionType, types.MethodDescriptorType, types.ClassMethodDescriptorType)) + # In PyPy 7.3.1, functions from extensions are not + # instances of the above abstract types. +- or v.__class__.__name__ == 'builtin_function_or_method'): ++ or (hasattr(v, "__class__") and v.__class__.__name__ == 'builtin_function_or_method')): + f = self.Function(self, k, parent) + f.parentMod = parentMod + f.docstring = v.__doc__ + f.decorators = None +- f.signature = Signature() ++ try: ++ f.signature = signature(v) ++ except Exception: ++ f.text_signature = (getattr(v, "__text_signature__") or "") + " (INVALID)" ++ f.signature = None ++ f.is_async = False ++ f.annotations = { ++ name: None for name in (f.signature.parameters if f.signature else {}) ++ } + self.addObject(f) + elif isinstance(v, type): + c = self.Class(self, k, parent) +@@ -912,7 +922,8 @@ class System: + mod.state = ProcessingState.PROCESSED + head = self.processing_modules.pop() + assert head == mod.fullName() +- self.unprocessed_modules.remove(mod) ++ if mod in self.unprocessed_modules: ++ self.unprocessed_modules.remove(mod) + self.progress( + 'process', + self.module_count - len(self.unprocessed_modules), +diff --git a/pydoctor/templatewriter/pages/__init__.py b/pydoctor/templatewriter/pages/__init__.py +index e5f4050..3d326b3 100644 +--- a/pydoctor/templatewriter/pages/__init__.py ++++ b/pydoctor/templatewriter/pages/__init__.py +@@ -49,7 +49,7 @@ def format_decorators(obj: Union[model.Function, model.Attribute]) -> Iterator[A + + def signature(function: model.Function) -> str: + """Return a nicely-formatted source-like function signature.""" +- return str(function.signature) ++ return str(function.signature) if function.signature else function.text_signature or "(...)" + + class DocGetter: + """L{epydoc2stan} bridge.""" +diff --git a/pydoctor/templates/common.html b/pydoctor/templates/common.html +index e5f4050..3d326b3 100644 +--- a/pydoctor/templates/common.html ++++ b/pydoctor/templates/common.html +@@ -63,7 +63,7 @@ + +